From b4b1c740655bc12fa4ec6d2e52e2e64467b0f67d Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Sun, 1 May 2022 16:51:42 +0200 Subject: [PATCH 1/7] Wrap invalid field names in quotes If a struct field name would be an invalid Typescript ident, the field name is wrapped in quotes. --- macros/src/types/named.rs | 5 +++-- macros/src/utils.rs | 12 ++++++++++++ ts-rs/tests/struct_rename.rs | 9 ++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 751ea9154..c942aaa2e 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -6,7 +6,7 @@ use crate::{ attr::{FieldAttr, Inflection, StructAttr}, deps::Dependencies, types::generics::{format_generics, format_type}, - utils::to_ts_ident, + utils::{raw_name_to_ts_field, to_ts_ident}, DerivedTS, }; @@ -107,9 +107,10 @@ fn format_field( (None, Some(rn)) => rn.apply(&field_name), (None, None) => field_name, }; + let valid_name = raw_name_to_ts_field(name); formatted_fields.push(quote! { - format!("{}{}: {},", #name, #optional_annotation, #formatted_ty) + format!("{}{}: {},", #valid_name, #optional_annotation, #formatted_ty) }); Ok(()) diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 373db55b1..c02ba8f67 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -55,6 +55,18 @@ pub fn to_ts_ident(ident: &Ident) -> String { } } +/// Convert an arbitrary name to a valid Typescript field name. +/// +/// If the name contains special characters it will be wrapped in quotes. +pub fn raw_name_to_ts_field(value: String) -> String { + let has_invalid_chars = value.chars().any(|c| !c.is_alphanumeric()); + if has_invalid_chars { + format!(r#""{value}""#) + } else { + value + } +} + /// Parse all `#[ts(..)]` attributes from the given slice. pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result> where diff --git a/ts-rs/tests/struct_rename.rs b/ts-rs/tests/struct_rename.rs index 6c2059e91..7b0be642b 100644 --- a/ts-rs/tests/struct_rename.rs +++ b/ts-rs/tests/struct_rename.rs @@ -9,7 +9,14 @@ struct Rename { b: i32, } +#[derive(serde::Serialize, TS)] +struct RenameSerdeSpecialChar { + #[serde(rename = "a/b")] + b: i32, +} + #[test] fn test() { - assert_eq!(Rename::inline(), "{ A: number, B: number, }") + assert_eq!(Rename::inline(), "{ A: number, B: number, }"); + assert_eq!(RenameSerdeSpecialChar::inline(), r#"{ "a/b": number, }"#); } From f85f15008ce3595067c41b4a51564cc4afef54fa Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Sun, 1 May 2022 14:53:45 +0200 Subject: [PATCH 2/7] Implement ordered-float support Adds support for the ordered-float crate with a new "ordered-float-impl" feature. --- README.md | 8 ++++++++ ts-rs/Cargo.toml | 2 ++ ts-rs/src/lib.rs | 10 ++++++++++ 3 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 3fc4bdaf9..e3d6c041c 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ When running `cargo test`, the TypeScript bindings will be exported to the file - serde compatibility - generic types +### limitations +- generic fields cannot be inlined or flattened (#56) +- type aliases must not alias generic types (#70) + ### cargo features - `serde-compat` (default) @@ -94,6 +98,10 @@ When running `cargo test`, the TypeScript bindings will be exported to the file Implement `TS` for `IndexMap` and `IndexSet` from indexmap +- `ordered-float-impl` + + Implement `TS` for `OrderedFloat` from ordered_float + If there's a type you're dealing with which doesn't implement `TS`, use `#[ts(type = "..")]` or open a PR. ### serde compatability diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index ff840aa18..690d8ffa6 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -21,6 +21,7 @@ serde-compat = ["ts-rs-macros/serde-compat"] format = ["dprint-plugin-typescript"] default = ["serde-compat"] indexmap-impl = ["indexmap"] +ordered-float-impl = ["ordered-float"] [dev-dependencies] serde = { version = "1.0", features = ["derive"] } @@ -36,3 +37,4 @@ bson = { version = "2.2.0", optional = true, features = ["uuid-0_8"], default-fe bytes = { version = "1.0", optional = true } thiserror = "1" indexmap = { version = "1.6.1", optional = true } +ordered-float = { version = "3.0.0", optional = true } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 49f46c67f..5249f5e97 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -96,6 +96,10 @@ //! //! Implement `TS` for `IndexMap` and `IndexSet` from indexmap //! +//! - `ordered-float-impl` +//! +//! Implement `TS` for `OrderedFloat` from ordered_float +//! //! If there's a type you're dealing with which doesn't implement `TS`, use `#[ts(type = "..")]` or open a PR. //! //! ## serde compatability @@ -536,6 +540,12 @@ impl_primitives! { bigdecimal::BigDecimal => "string" } #[cfg(feature = "uuid-impl")] impl_primitives! { uuid::Uuid => "string" } +#[cfg(feature = "ordered-float-impl")] +impl_primitives! { ordered_float::OrderedFloat => "number" } + +#[cfg(feature = "ordered-float-impl")] +impl_primitives! { ordered_float::OrderedFloat => "number" } + #[cfg(feature = "bson-uuid-impl")] impl_primitives! { bson::Uuid => "string" } From 586c0ef9211e99a32833aebe71e265fd6f545dbe Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 29 May 2022 12:49:53 +0200 Subject: [PATCH 3/7] clean up past PRs --- CONTRIBUTING.md | 2 +- macros/src/utils.rs | 11 +++++++++-- ts-rs/src/export.rs | 24 ++++++++++++------------ ts-rs/src/lib.rs | 5 ++++- ts-rs/tests/export_manually.rs | 4 ++-- ts-rs/tests/generics.rs | 2 +- ts-rs/tests/imports.rs | 22 +++++++++++----------- 7 files changed, 40 insertions(+), 30 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3afb77aab..f1d62df72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ If you are unsure what to work on or want to discuss your idea, feel free to ope ### Documentation After implementing a new feature, please document it in the doc comment on `TS` in `ts_rs/lib.rs`. `README.md` is generated from the module doc comment in `ts_rs/lib.rs`. If you added/updated documentation there, run -`cargo readme > ../README.md` from the `ts_rs/` directory. +`cargo readme > ../README.md` from the `ts-rs/` directory. You can install `cargo readme` by running `cargo install cargo-readme`. diff --git a/macros/src/utils.rs b/macros/src/utils.rs index c02ba8f67..0f32f2c43 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -59,8 +59,15 @@ pub fn to_ts_ident(ident: &Ident) -> String { /// /// If the name contains special characters it will be wrapped in quotes. pub fn raw_name_to_ts_field(value: String) -> String { - let has_invalid_chars = value.chars().any(|c| !c.is_alphanumeric()); - if has_invalid_chars { + let valid = value + .chars() + .all(|c| c.is_alphanumeric() || c == '_' || c == '$') + && value + .chars() + .next() + .map(|first| !first.is_numeric()) + .unwrap_or(true); + if !valid { format!(r#""{value}""#) } else { value diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 0b023c6bf..ed968ed7e 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -26,8 +26,7 @@ pub enum ExportError { ManifestDirNotSet, } -/// Export `T` to the file specified by the `#[ts(export = ..)]` attribute and/or the `out_dir` -/// setting in the `ts.toml` config file. +/// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute pub(crate) fn export_type() -> Result<(), ExportError> { let path = output_path::()?; export_type_to::(&path) @@ -35,7 +34,17 @@ pub(crate) fn export_type() -> Result<(), ExportError> { /// Export `T` to the file specified by the `path` argument. pub(crate) fn export_type_to>(path: P) -> Result<(), ExportError> { - let buffer = export_type_to_string::()?; + #[allow(unused_mut)] + let mut buffer = export_type_to_string::()?; + + // format output + #[cfg(feature = "format")] + { + use dprint_plugin_typescript::{configuration::ConfigurationBuilder, format_text}; + + let fmt_cfg = ConfigurationBuilder::new().deno().build(); + buffer = format_text(path.as_ref(), &buffer, &fmt_cfg).map_err(Formatting)?; + } if let Some(parent) = path.as_ref().parent() { std::fs::create_dir_all(parent)?; @@ -50,15 +59,6 @@ pub(crate) fn export_type_to_string() -> Result(&mut buffer)?; generate_decl::(&mut buffer); - // format output - #[cfg(feature = "format")] - { - use dprint_plugin_typescript::{configuration::ConfigurationBuilder, format_text}; - - let fmt_cfg = ConfigurationBuilder::new().deno().build(); - buffer = format_text(path.as_ref(), &buffer, &fmt_cfg).map_err(Formatting)?; - } - Ok(buffer) } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 8e3480826..36eafbc89 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -173,7 +173,8 @@ mod export; /// `cargo test` /// /// - `#[ts(export_to = "..")]`: -/// Specifies where the type should be exported to. Defaults to `bindings/.ts`. +/// Specifies where the type should be exported to. Defaults to `bindings/.ts`. +/// If the provided path ends in a trailing `/`, it is interpreted as a directory. /// /// - `#[ts(rename = "..")]`: /// Sets the typescript name of the generated type @@ -283,6 +284,8 @@ pub trait TS: 'static { export::export_type_to::(path) } + /// Manually generate bindings for this type, returning a [`String`]. + /// This function does not format the output, even if the `format` feature is enabled. fn export_to_string() -> Result { export::export_type_to_string::() } diff --git a/ts-rs/tests/export_manually.rs b/ts-rs/tests/export_manually.rs index c41f9d277..aaae5cf56 100644 --- a/ts-rs/tests/export_manually.rs +++ b/ts-rs/tests/export_manually.rs @@ -26,7 +26,7 @@ fn export_manually() { let expected_content = if cfg!(feature = "format") { concat!( - "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", + "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n", "export interface User {\n", " name: string;\n", " age: number;\n", @@ -51,7 +51,7 @@ fn export_manually_dir() { let expected_content = if cfg!(feature = "format") { concat!( - "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", + "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n", "export interface UserDir {\n", " name: string;\n", " age: number;\n", diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index 77b0a794c..93bd70905 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -126,7 +126,7 @@ fn generic_struct() { #[test] #[ignore] -// https://github.com/Aleph-Alpha/ts-rs/issues/56 +// https://github.com/Aleph-Alpha/ts-rs/issues/56 TODO fn inline() { #[derive(TS)] struct Generic { diff --git a/ts-rs/tests/imports.rs b/ts-rs/tests/imports.rs index 134f486af..805f0669b 100644 --- a/ts-rs/tests/imports.rs +++ b/ts-rs/tests/imports.rs @@ -28,19 +28,19 @@ pub enum TestEnum { fn test_def() { // The only way to get access to how the imports look is to export the type and load the exported file TestEnum::export().unwrap(); - let text = String::from_utf8(std::fs::read(TestEnum::EXPORT_TO.unwrap()).unwrap()).unwrap(); + let text = std::fs::read_to_string(TestEnum::EXPORT_TO.unwrap()).unwrap(); // Checks to make sure imports are ordered and deduplicated - assert_eq!( - text, - "\ -import type { TestTypeA } from \"./ts_rs_test_type_a\"; -import type { TestTypeB } from \"./ts_rs_test_type_b\"; - -export type TestEnum = { C: { value: TestTypeB } } | { - A1: { value: TestTypeA }; -} | { A2: { value: TestTypeA } }; -" + assert_eq!(text, + concat!( + "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", + "import type { TestTypeA } from \"./ts_rs_test_type_a\";\n", + "import type { TestTypeB } from \"./ts_rs_test_type_b\";\n", + "\n", + "export type TestEnum = { C: { value: TestTypeB } } | {\n", + " A1: { value: TestTypeA };\n", + "} | { A2: { value: TestTypeA } };\n" + ) ); std::fs::remove_file(TestEnum::EXPORT_TO.unwrap()).unwrap(); From a4c94e823713f39cacd250f05ea352211f6246d4 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 29 May 2022 12:56:47 +0200 Subject: [PATCH 4/7] fix rename test --- ts-rs/tests/struct_rename.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/ts-rs/tests/struct_rename.rs b/ts-rs/tests/struct_rename.rs index 7b0be642b..12039d05d 100644 --- a/ts-rs/tests/struct_rename.rs +++ b/ts-rs/tests/struct_rename.rs @@ -2,21 +2,26 @@ use ts_rs::TS; -#[derive(TS)] -#[ts(rename_all = "UPPERCASE")] -struct Rename { - a: i32, - b: i32, -} +#[test] +fn rename_all() { + #[derive(TS)] + #[ts(rename_all = "UPPERCASE")] + struct Rename { + a: i32, + b: i32, + } -#[derive(serde::Serialize, TS)] -struct RenameSerdeSpecialChar { - #[serde(rename = "a/b")] - b: i32, + assert_eq!(Rename::inline(), "{ A: number, B: number, }"); } +#[cfg(feature = "serde-compat")] #[test] -fn test() { - assert_eq!(Rename::inline(), "{ A: number, B: number, }"); +fn serde_rename_special_char() { + #[derive(serde::Serialize, TS)] + struct RenameSerdeSpecialChar { + #[serde(rename = "a/b")] + b: i32, + } + assert_eq!(RenameSerdeSpecialChar::inline(), r#"{ "a/b": number, }"#); } From 3555ea5c20b145f1b676b8f5d66d5f12889b168d Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 29 May 2022 12:57:53 +0200 Subject: [PATCH 5/7] run CI on pull request --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cabc61baa..d980a125b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -on: [push] +on: [push, pull_request] name: Test jobs: test: From ce3e108d1fc6c9cb00781cb7eb356a78fb73d0a1 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 29 May 2022 12:58:59 +0200 Subject: [PATCH 6/7] manually merge #103 --- ts-rs/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 36eafbc89..32ea04623 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -538,6 +538,7 @@ impl_wrapper!(impl TS for std::cell::Cell); impl_wrapper!(impl TS for std::cell::RefCell); impl_wrapper!(impl TS for std::sync::Mutex); impl_wrapper!(impl TS for std::sync::Weak); +impl_wrapper!(impl TS for std::marker::PhantomData); impl_tuples!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); From 81d83aeeb209901a9b321346cd1f43c5ead06391 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 29 May 2022 13:02:51 +0200 Subject: [PATCH 7/7] bump version to 6.2.0 --- macros/Cargo.toml | 2 +- ts-rs/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/macros/Cargo.toml b/macros/Cargo.toml index b5fd1e850..1568b792e 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ts-rs-macros" -version = "6.1.2" +version = "6.2.0" authors = ["Moritz Bischof "] edition = "2021" description = "derive macro for ts-rs" diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index 690d8ffa6..825fb784d 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ts-rs" -version = "6.1.2" +version = "6.2.0" authors = ["Moritz Bischof "] edition = "2021" license = "MIT" @@ -28,7 +28,7 @@ serde = { version = "1.0", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } [dependencies] -ts-rs-macros = { version = "6.1.2", path = "../macros" } +ts-rs-macros = { version = "6.2.0", path = "../macros" } dprint-plugin-typescript = { version = "0.43", optional = true } chrono = { version = "0.4.19", optional = true } bigdecimal = { version = ">=0.0.13, < 0.4.0", features = ["serde"], optional = true }