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: 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/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/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/macros/src/types/named.rs b/macros/src/types/named.rs index ae7172811..ec42cc5b8 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, }; @@ -108,6 +108,7 @@ fn format_field( (None, Some(rn)) => rn.apply(&field_name), (None, None) => field_name, }; + let valid_name = raw_name_to_ts_field(name); let doc_string = match doc_string { Some(s) => format!("\n/**\n* {}\n*/\n", s), @@ -115,7 +116,7 @@ fn format_field( }; formatted_fields.push(quote! { - format!("{}{}{}: {},", #doc_string, #name, #optional_annotation, #formatted_ty) + format!("{}{}{}: {},", #doc_string, #valid_name, #optional_annotation, #formatted_ty) }); Ok(()) diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 373db55b1..0f32f2c43 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -55,6 +55,25 @@ 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 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 + } +} + /// Parse all `#[ts(..)]` attributes from the given slice. pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result> where diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index ff840aa18..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" @@ -21,13 +21,14 @@ 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"] } 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 } @@ -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/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 247641c18..d96228259 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 @@ -169,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 @@ -282,6 +287,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::() } @@ -534,6 +541,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); @@ -543,6 +551,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" } 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(); diff --git a/ts-rs/tests/struct_rename.rs b/ts-rs/tests/struct_rename.rs index 6c2059e91..12039d05d 100644 --- a/ts-rs/tests/struct_rename.rs +++ b/ts-rs/tests/struct_rename.rs @@ -2,14 +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, + } + + 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, }"#); }