Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions macros/src/attr/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ pub enum Tagged<'a> {
Untagged,
}

#[derive(Copy, Clone)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Repr {
Int,
Name,
ConstObject,
}

impl EnumAttr {
Expand Down Expand Up @@ -228,7 +229,7 @@ impl Attr for EnumAttr {
}

if let Optional::Optional { .. } = self.optional_fields {
syn_err!("`optional_fields` is not compatible with `as`");
syn_err!("`optional_fields` is not compatible with `repr`");
}
}

Expand Down
3 changes: 2 additions & 1 deletion macros/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ fn parse_repr(input: ParseStream) -> Result<Repr> {
let ident = content.parse::<Ident>()?;
match ident.to_string().as_str() {
"name" => Ok(Repr::Name),
_ => syn_err!(span; "expected `name`"),
"const_object" => Ok(Repr::ConstObject),
_ => syn_err!(span; "expected `name` or `const_object`"),
}
}
72 changes: 72 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,36 @@ impl DerivedTS {

buffer.trim_end_matches(['|', ' ']).into()
},
Some(Repr::ConstObject) => quote! {
let variants = #inline;
let mut variants = variants.split(',').map(|v| v.trim()).peekable();

if variants.peek().is_none() {
return "never".into()
}

let mut buffer = String::new();
let mut latest = None::<isize>;

for variant in variants {
if let Some((_name, explicit_val)) = variant.split_once('=') {
let explicit_val = explicit_val.trim();

if let Ok(val) = explicit_val.parse::<isize>() {
buffer.push_str(&format!("{} | ", val));
latest = Some(val);
} else {
buffer.push_str(&format!("{} | ", explicit_val));
}
} else {
let new_val = latest.map(|x| x + 1).unwrap_or(0);
buffer.push_str(&format!("{} | ", new_val));
latest = Some(new_val);
}
}

buffer.trim_end_matches(['|', ' ']).into()
},
None => quote!(#inline),
};

Expand All @@ -316,6 +346,48 @@ impl DerivedTS {
let crate_rename = &self.crate_rename;
let name = &self.ts_name;

if self.ts_enum == Some(Repr::ConstObject) {
let inline = &self.inline;
return quote! {
fn decl_concrete(cfg: &#crate_rename::Config) -> String {
let variants = #inline;
let mut variants = variants.split(',').map(|v| v.trim()).peekable();

if variants.peek().is_none() {
return format!("const {} = {{}} as const;\nexport type {} = never;", #name, #name)
}

let mut buffer = String::new();
let mut latest = None::<isize>;

for variant in variants {
if let Some((name, explicit_val)) = variant.split_once('=') {
let name = name.trim();
let explicit_val = explicit_val.trim();

if let Ok(val) = explicit_val.parse::<isize>() {
buffer.push_str(&format!("{}: {}, ", name, val));
latest = Some(val);
} else {
buffer.push_str(&format!("{}: {}, ", name, explicit_val));
}
} else {
let new_val = latest.map(|x| x + 1).unwrap_or(0);
buffer.push_str(&format!("{}: {}, ", variant, new_val));
latest = Some(new_val);
}
}

let obj = format!("{{ {} }}", buffer.trim_end_matches([',', ' ']));
format!("const {} = {} as const;\nexport type {} = (typeof {})[keyof typeof {}];", #name, obj, #name, #name, #name)
}

fn decl(cfg: &#crate_rename::Config) -> String {
<Self as #crate_rename::TS>::decl_concrete(cfg)
}
};
}

if self.ts_enum.is_some() {
let inline = &self.inline;
return quote! {
Expand Down
15 changes: 11 additions & 4 deletions macros/src/types/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {

let mut formatted_variants = Vec::new();
let mut dependencies = Dependencies::new(crate_rename.clone());
let is_int_enum = s.variants.iter().any(|v| v.discriminant.is_some());

for variant in &s.variants {
format_variant(
&mut formatted_variants,
&mut dependencies,
&enum_attr,
variant,
is_int_enum,
)?;
}

Expand Down Expand Up @@ -74,6 +76,7 @@ fn format_variant(
dependencies: &mut Dependencies,
enum_attr: &EnumAttr,
variant: &Variant,
is_int_enum: bool,
) -> syn::Result<()> {
let crate_rename = enum_attr.crate_rename();

Expand Down Expand Up @@ -101,12 +104,16 @@ fn format_variant(
};

if let Some(ref repr) = enum_attr.repr {
let formatted = match (repr, &variant.discriminant) {
(Repr::Int, Some((_, value))) => {
let formatted = match (repr, &variant.discriminant, is_int_enum) {
(Repr::Int, Some((_, value)), _) | (Repr::ConstObject, Some((_, value)), true) => {
quote!(format!("\"{}\" = {}", #ts_name, #value))
}
(Repr::Int, None) => quote!(format!("\"{}\"", #ts_name)),
(Repr::Name, _) => quote!(format!("\"{}\" = \"{}\"", #ts_name, #ts_name)),
(Repr::Int, None, _) | (Repr::ConstObject, None, true) => {
quote!(format!("\"{}\"", #ts_name))
}
(Repr::Name, _, _) | (Repr::ConstObject, _, false) => {
quote!(format!("\"{}\" = \"{}\"", #ts_name, #ts_name))
}
};

formatted_variants.push(formatted);
Expand Down
7 changes: 5 additions & 2 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,12 @@ mod tokio;
///
/// - **`#[ts(repr(enum))]`** \
/// Exports the enum as a TypeScript enum instead of type union. \
/// Discriminants (`= {integer}`) are included in the exported enum's variants
/// Discriminants (`= {integer}`) are included in the exported enum's variants. \
/// If `#[ts(repr(enum = name))]` is used, all variants without a discriminant will be exported
/// as `VariantName = "VariantName"`
/// as `VariantName = "VariantName"`. \
/// If `#[ts(repr(enum = const_object))]` or `#[ts(repr(enum = "const-object"))]` is used, the enum is exported as a TypeScript `const` object
/// alongside a type alias for its values, allowing it to be used at runtime. All variants of the enum
/// must be unit variants.
///
/// ### enum variant attributes
///
Expand Down
5 changes: 4 additions & 1 deletion ts-rs/tests/integration/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ struct Interface {
#[test]
fn free() {
let cfg = Config::from_env();
assert_eq!(<[String; 4]>::inline(&cfg), "[string, string, string, string]")
assert_eq!(
<[String; 4]>::inline(&cfg),
"[string, string, string, string]"
)
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion ts-rs/tests/integration/bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ struct User {
#[test]
fn bson() {
let cfg = Config::from_env();
assert_eq!(User::decl(&cfg), "type User = { _id: string, _uuid: string, };")
assert_eq!(
User::decl(&cfg),
"type User = { _id: string, _uuid: string, };"
)
}
5 changes: 4 additions & 1 deletion ts-rs/tests/integration/concrete_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ mod simple {
#[test]
fn simple() {
let cfg = Config::from_env();
assert_eq!(Simple::<String>::decl(&cfg), "type Simple = { t: number, };");
assert_eq!(
Simple::<String>::decl(&cfg),
"type Simple = { t: number, };"
);
assert_eq!(
WithOption::<String>::decl(&cfg),
"type WithOption = { opt: number | null, };"
Expand Down
5 changes: 4 additions & 1 deletion ts-rs/tests/integration/enum_variant_annotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ pub enum C {
#[test]
fn test_enum_variant_with_tag() {
let cfg = Config::from_env();
assert_eq!(C::inline(&cfg), r#"{ "kind": "SQUARE_THING", name: string, }"#);
assert_eq!(
C::inline(&cfg),
r#"{ "kind": "SQUARE_THING", name: string, }"#
);
}

#[cfg(feature = "serde-compat")]
Expand Down
5 changes: 4 additions & 1 deletion ts-rs/tests/integration/generics_flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ fn flattened_generic_parameters() {
}

let cfg = Config::from_env();
assert_eq!(Item::<()>::decl(&cfg), "type Item<D> = { id: string, } & D;");
assert_eq!(
Item::<()>::decl(&cfg),
"type Item<D> = { id: string, } & D;"
);
assert_eq!(
TwoParameters::<(), ()>::decl(&cfg),
"type TwoParameters<A, B> = { id: string, ab: [A, B], } & A & B;"
Expand Down
5 changes: 4 additions & 1 deletion ts-rs/tests/integration/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ struct List {
#[test]
fn list() {
let cfg = Config::from_env();
assert_eq!(List::decl(&cfg), "type List = { data: Array<number> | null, };");
assert_eq!(
List::decl(&cfg),
"type List = { data: Array<number> | null, };"
);
}
1 change: 1 addition & 0 deletions ts-rs/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ mod ranges;
mod raw_idents;
mod recursion_limit;
mod references;
mod repr_const_object;
mod repr_enum;
mod same_file_export;
mod self_referential;
Expand Down
10 changes: 8 additions & 2 deletions ts-rs/tests/integration/optional_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ fn in_struct() {
let b = "b?: number | null";
let c = "c: number | null";
let cfg = Config::from_env();
assert_eq!(OptionalInStruct::inline(&cfg), format!("{{ {a}, {b}, {c}, }}"));
assert_eq!(
OptionalInStruct::inline(&cfg),
format!("{{ {a}, {b}, {c}, }}")
);
}

#[derive(Serialize, TS)]
Expand Down Expand Up @@ -105,7 +108,10 @@ fn inline() {
let b = "b?: number | null";
let c = "c: number | null";
let cfg = Config::from_env();
assert_eq!(Inline::inline(&cfg), format!("{{ x: {{ {a}, {b}, {c}, }}, }}"));
assert_eq!(
Inline::inline(&cfg),
format!("{{ x: {{ {a}, {b}, {c}, }}, }}")
);
}

type Foo = Option<i32>;
Expand Down
92 changes: 92 additions & 0 deletions ts-rs/tests/integration/repr_const_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use ts_rs::{Config, TS};

#[derive(TS)]
#[ts(export, export_to = "repr_const_object/", repr(enum = const_object))]
enum Foo {
A = 1,
B = 2,
}

#[derive(TS)]
#[ts(export, export_to = "repr_const_object/", repr(enum = const_object))]
enum Bar {
A = 1,
B,
}

#[derive(TS)]
#[ts(export, export_to = "repr_const_object/", repr(enum = const_object))]
enum Baz {
A,
B,
}

#[derive(TS)]
#[ts(export, export_to = "repr_const_object/", rename_all = "snake_case", repr(enum = const_object))]
enum SnakeCase {
EnumVariantFoo,
EnumVariantBar,
}

#[derive(TS)]
#[ts(export, export_to = "repr_const_object/", rename_all = "camelCase", repr(enum = const_object))]
enum CamelCase {
EnumVariantFoo,
EnumVariantBar,
}

#[derive(TS)]
#[ts(export, export_to = "repr_const_object/", rename_all = "kebab-case", repr(enum = const_object))]
enum KebabCase {
EnumVariantFoo,
EnumVariantBar,
}

#[test]
fn native_ts_enum_repr_const_object() {
let cfg = Config::from_env();
assert_eq!(
Foo::decl(&cfg),
"const Foo = { \"A\": 1, \"B\": 2 } as const;\nexport type Foo = (typeof Foo)[keyof typeof Foo];"
);
assert_eq!(
Bar::decl(&cfg),
"const Bar = { \"A\": 1, \"B\": 2 } as const;\nexport type Bar = (typeof Bar)[keyof typeof Bar];"
);
assert_eq!(
Baz::decl(&cfg),
"const Baz = { \"A\": \"A\", \"B\": \"B\" } as const;\nexport type Baz = (typeof Baz)[keyof typeof Baz];"
);
assert_eq!(
SnakeCase::decl(&cfg),
"const SnakeCase = { \"enum_variant_foo\": \"enum_variant_foo\", \"enum_variant_bar\": \"enum_variant_bar\" } as const;\nexport type SnakeCase = (typeof SnakeCase)[keyof typeof SnakeCase];"
);
assert_eq!(
CamelCase::decl(&cfg),
"const CamelCase = { \"enumVariantFoo\": \"enumVariantFoo\", \"enumVariantBar\": \"enumVariantBar\" } as const;\nexport type CamelCase = (typeof CamelCase)[keyof typeof CamelCase];"
);
assert_eq!(
KebabCase::decl(&cfg),
"const KebabCase = { \"enum-variant-foo\": \"enum-variant-foo\", \"enum-variant-bar\": \"enum-variant-bar\" } as const;\nexport type KebabCase = (typeof KebabCase)[keyof typeof KebabCase];"
);
}

#[test]
fn native_ts_enum_repr_const_object_inline() {
let cfg = Config::from_env();
assert_eq!(Foo::inline(&cfg), "1 | 2");
assert_eq!(Bar::inline(&cfg), "1 | 2");
assert_eq!(Baz::inline(&cfg), "\"A\" | \"B\"");
assert_eq!(
SnakeCase::inline(&cfg),
"\"enum_variant_foo\" | \"enum_variant_bar\""
);
assert_eq!(
CamelCase::inline(&cfg),
"\"enumVariantFoo\" | \"enumVariantBar\""
);
assert_eq!(
KebabCase::inline(&cfg),
"\"enum-variant-foo\" | \"enum-variant-bar\""
);
}
20 changes: 4 additions & 16 deletions ts-rs/tests/integration/repr_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,10 @@ enum KebabCase {
#[test]
fn native_ts_enum_repr() {
let cfg = Config::from_env();
assert_eq!(
Foo::decl(&cfg),
"enum Foo { \"A\" = 1, \"B\" = 2 }"
);
assert_eq!(
Bar::decl(&cfg),
"enum Bar { \"A\" = 1, \"B\" }"
);
assert_eq!(
Baz::decl(&cfg),
"enum Baz { \"A\", \"B\" }"
);
assert_eq!(
Biz::decl(&cfg),
"enum Biz { \"A\" = \"A\", \"B\" = \"B\" }"
);
assert_eq!(Foo::decl(&cfg), "enum Foo { \"A\" = 1, \"B\" = 2 }");
assert_eq!(Bar::decl(&cfg), "enum Bar { \"A\" = 1, \"B\" }");
assert_eq!(Baz::decl(&cfg), "enum Baz { \"A\", \"B\" }");
assert_eq!(Biz::decl(&cfg), "enum Biz { \"A\" = \"A\", \"B\" = \"B\" }");
assert_eq!(
SnakeCase::decl(&cfg),
"enum SnakeCase { \"enum_variant_foo\" = \"enum_variant_foo\", \"enum_variant_bar\" = \"enum_variant_bar\" }"
Expand Down
Loading