>();
for (_, dep) in deduplicated_deps {
- let rel_path = import_path(&path, Path::new(&dep.exported_to));
+ let dep_path = out_dir.as_ref().join(dep.output_path);
+ let rel_path = import_path(&path, &dep_path);
writeln!(
out,
"import type {{ {} }} from {:?};",
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 295287512..0954e6973 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -4,7 +4,7 @@
//! ts-rs
//!
//!
-//! generate typescript type declarations from rust types
+//! Generate typescript type declarations from rust types
//!
//!
//!
@@ -24,23 +24,23 @@
//!
//!
//!
-//! ## why?
+//! ## Why?
//! When building a web application in rust, data structures have to be shared between backend and frontend.
//! Using this library, you can easily generate TypeScript bindings to your rust structs & enums so that you can keep your
//! types in one place.
//!
//! ts-rs might also come in handy when working with webassembly.
//!
-//! ## how?
+//! ## How?
//! ts-rs exposes a single trait, `TS`. Using a derive macro, you can implement this interface for your types.
//! Then, you can use this trait to obtain the TypeScript bindings.
//! We recommend doing this in your tests.
//! [See the example](https://github.com/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs) and [the docs](https://docs.rs/ts-rs/latest/ts_rs/).
//!
-//! ## get started
+//! ## Get started
//! ```toml
//! [dependencies]
-//! ts-rs = "7.1"
+//! ts-rs = "8.1"
//! ```
//!
//! ```rust
@@ -56,7 +56,7 @@
//! ```
//! When running `cargo test`, the TypeScript bindings will be exported to the file `bindings/User.ts`.
//!
-//! ## features
+//! ## Features
//! - generate type declarations from rust structs
//! - generate union declarations from rust enums
//! - inline types
@@ -73,6 +73,7 @@
//! | format | Enables formatting of the generated TypeScript bindings.
Currently, this unfortunately adds quite a few dependencies. |
//! | no-serde-warnings | By default, warnings are printed during build if unsupported serde attributes are encountered.
Enabling this feature silences these warnings. |
//! | import-esm | When enabled,`import` statements in the generated file will have the `.js` extension in the end of the path to conform to the ES Modules spec.
Example: `import { MyStruct } from "./my_struct.js"` |
+//! | serde-json-impl | Implement `TS` for types from *serde_json* |
//! | chrono-impl | Implement `TS` for types from *chrono* |
//! | bigdecimal-impl | Implement `TS` for types from *bigdecimal* |
//! | url-impl | Implement `TS` for types from *url* |
@@ -89,7 +90,7 @@
//! If there's a type you're dealing with which doesn't implement `TS`, use either
//! `#[ts(as = "..")]` or `#[ts(type = "..")]`, or open a PR.
//!
-//! ## serde compatability
+//! ## `serde` compatability
//! With the `serde-compat` feature (enabled by default), serde attributes can be parsed for enums and structs.
//! Supported serde attributes:
//! - `rename`
@@ -107,18 +108,13 @@
//!
//! When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled.
//!
-//! ## contributing
+//! ## Contributing
//! Contributions are always welcome!
//! Feel free to open an issue, discuss using GitHub discussions or open a PR.
//! [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md)
//!
-//! ## todo
-//! - [x] serde compatibility layer
-//! - [x] documentation
-//! - [x] use typescript types across files
-//! - [x] more enum representations
-//! - [x] generics
-//! - [x] don't require `'static`
+//! ## MSRV
+//! The Minimum Supported Rust Version for this crate is 1.75.0
use std::{
any::TypeId,
@@ -140,6 +136,8 @@ use crate::typelist::TypeList;
#[cfg(feature = "chrono-impl")]
mod chrono;
mod export;
+#[cfg(feature = "serde-json-impl")]
+mod serde_json;
pub mod typelist;
/// A type which can be represented in TypeScript.
@@ -149,14 +147,24 @@ pub mod typelist;
///
/// ### exporting
/// Because Rusts procedural macros are evaluated before other compilation steps, TypeScript
-/// bindings cannot be exported during compile time.
+/// bindings __cannot__ be exported during compile time.
+///
/// Bindings can be exported within a test, which ts-rs generates for you by adding `#[ts(export)]`
-/// to a type you wish to export to a file.
-/// If, for some reason, you need to do this during runtime, you can call [`TS::export`] yourself.
+/// to a type you wish to export to a file.
+/// When `cargo test` is run, all types annotated with `#[ts(export)]` and all of their
+/// dependencies will be written to `TS_RS_EXPORT_DIR`, or `./bindings` by default.
+///
+/// For each individual type, path and filename within the output directory can be changed using
+/// `#[ts(export_to = "...")]`. By default, the filename will be derived from the name of the type.
///
-/// **Note:**
-/// Annotating a type with `#[ts(export)]` (or exporting it during runtime using
-/// [`TS::export`]) will cause all of its dependencies to be exported as well.
+/// If, for some reason, you need to do this during runtime or cannot use `#[ts(export)]`, bindings
+/// can be exported manually:
+///
+/// | Function | Includes Dependencies | To |
+/// |-----------------------|-----------------------|--------------------|
+/// | [`TS::export`] | ❌ | `TS_RS_EXPORT_DIR` |
+/// | [`TS::export_all`] | ✔️ | `TS_RS_EXPORT_DIR` |
+/// | [`TS::export_all_to`] | ✔️ | _custom_ |
///
/// ### serde compatibility
/// By default, the feature `serde-compat` is enabled.
@@ -167,6 +175,10 @@ pub mod typelist;
/// ### container attributes
/// attributes applicable for both structs and enums
///
+/// - **`#[ts(crate = "..")]`**
+/// Generates code which references the module passed to it instead of defaulting to `::ts_rs`
+/// This is useful for cases where you have to re-export the crate.
+///
/// - **`#[ts(export)]`**
/// Generates a test which will export the type, by default to `bindings/.ts` when running
/// `cargo test`. The default base directory can be overridden with the `TS_RS_EXPORT_DIR` environment variable.
@@ -187,13 +199,78 @@ pub mod typelist;
/// Note that you need to add the `export` attribute as well, in order to generate a test which exports the type.
///
///
+/// - **`#[ts(as = "..")]`**
+/// Overrides the type used in Typescript, using the provided Rust type instead.
+/// This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually
+///
+///
+/// - **`#[ts(type = "..")]`**
+/// Overrides the type used in TypeScript.
+/// This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually
+///
+///
/// - **`#[ts(rename = "..")]`**
/// Sets the typescript name of the generated type
///
///
/// - **`#[ts(rename_all = "..")]`**
-/// Rename all fields/variants of the type.
-/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+/// Rename all fields/variants of the type.
+/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
+///
+///
+/// - **`#[ts(concrete(..)]`**
+/// Disables one ore more generic type parameters by specifying a concrete type for them.
+/// The resulting TypeScript definition will not be generic over these parameters and will use the
+/// provided type instead.
+/// This is especially useful for generic types containing associated types. Since TypeScript does
+/// not have an equivalent construct to associated types, we cannot generate a generic definition
+/// for them. Using `#[ts(concrete(..)]`, we can however generate a non-generic definition.
+/// Example:
+/// ```
+/// # use ts_rs::TS;
+/// ##[derive(TS)]
+/// ##[ts(concrete(I = std::vec::IntoIter))]
+/// struct SearchResult(Vec);
+/// // will always generate `type SearchResult = Array`.
+/// ```
+///
+///
+/// - **`#[ts(bound)]`**
+/// Override the bounds generated on the `TS` implementation for this type. This is useful in
+/// combination with `#[ts(concrete)]`, when the type's generic parameters aren't directly used
+/// in a field or variant.
+///
+/// Example:
+/// ```
+/// # use ts_rs::TS;
+///
+/// trait Container {
+/// type Value: TS;
+/// }
+///
+/// struct MyContainer;
+///
+/// ##[derive(TS)]
+/// struct MyValue;
+///
+/// impl Container for MyContainer {
+/// type Value = MyValue;
+/// }
+///
+/// ##[derive(TS)]
+/// ##[ts(export, concrete(C = MyContainer))]
+/// struct Inner {
+/// value: C::Value,
+/// }
+///
+/// ##[derive(TS)]
+/// // Without `#[ts(bound)]`, `#[derive(TS)]` would generate an unnecessary
+/// // `C: TS` bound
+/// ##[ts(export, concrete(C = MyContainer), bound = "C::Value: TS")]
+/// struct Outer {
+/// inner: Inner,
+/// }
+/// ```
///
///
/// ### struct attributes
@@ -254,13 +331,13 @@ pub mod typelist;
///
/// - **`#[ts(rename_all = "..")]`**
/// Rename all variants of this enum.
-/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
///
///
/// - **`#[ts(rename_all_fieds = "..")]`**
/// Renames the fields of all the struct variants of this enum. This is equivalent to using
/// `#[ts(rename_all = "..")]` on all of the enum's variants.
-/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
///
///
/// ### enum variant attributes
@@ -280,7 +357,7 @@ pub mod typelist;
///
/// - **`#[ts(rename_all = "..")]`**
/// Renames all the fields of a struct variant.
-/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
///
pub trait TS {
/// If this type does not have generic parameters, then `WithoutGenerics` should just be `Self`.
@@ -380,51 +457,124 @@ pub trait TS {
deps
}
- /// Manually export this type to a file.
- /// The output file can be specified by annotating the type with `#[ts(export_to = ".."]`.
- /// By default, the filename will be derived from the types name.
+ /// Manually export this type to the filesystem.
+ /// To export this type together with all of its dependencies, use [`TS::export_all`].
+ ///
+ /// # Automatic Exporting
+ /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
+ /// exported automatically whenever `cargo test` is run.
+ /// In that case, there is no need to manually call this function.
///
- /// When a type is annotated with `#[ts(export)]`, it is exported automatically within a test.
- /// This function is only usefull if you need to export the type outside of the context of a
- /// test.
+ /// # Target Directory
+ /// The target directory to which the type will be exported may be changed by setting the
+ /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used.
+ ///
+ /// To specify a target directory manually, use [`TS::export_all_to`], which also exports all
+ /// dependencies.
+ ///
+ /// To alter the filename or path of the type within the target directory,
+ /// use `#[ts(export_to = "...")]`.
fn export() -> Result<(), ExportError>
where
Self: 'static,
{
- export::export_type_with_dependencies::()
+ let path = Self::default_output_path()
+ .ok_or_else(std::any::type_name::)
+ .map_err(ExportError::CannotBeExported)?;
+
+ export::export_to::(path)
}
- /// Manually export this type to a file with a file with the specified path. This
- /// function will ignore the `#[ts(export_to = "..)]` attribute.
- fn export_to(path: impl AsRef) -> Result<(), ExportError>
+ /// Manually export this type to the filesystem, together with all of its dependencies.
+ /// To export only this type, without its dependencies, use [`TS::export`].
+ ///
+ /// # Automatic Exporting
+ /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
+ /// exported automatically whenever `cargo test` is run.
+ /// In that case, there is no need to manually call this function.
+ ///
+ /// # Target Directory
+ /// The target directory to which the types will be exported may be changed by setting the
+ /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used.
+ ///
+ /// To specify a target directory manually, use [`TS::export_all_to`].
+ ///
+ /// To alter the filenames or paths of the types within the target directory,
+ /// use `#[ts(export_to = "...")]`.
+ fn export_all() -> Result<(), ExportError>
where
Self: 'static,
{
- export::export_type_to::(path)
+ export::export_all_into::(&*export::default_out_dir())
+ }
+
+ /// Manually export this type into the given directory, together with all of its dependencies.
+ /// To export only this type, without its dependencies, use [`TS::export`].
+ ///
+ /// Unlike [`TS::export_all`], this function disregards `TS_RS_EXPORT_DIR`, using the provided
+ /// directory instead.
+ ///
+ /// To alter the filenames or paths of the types within the target directory,
+ /// use `#[ts(export_to = "...")]`.
+ ///
+ /// # Automatic Exporting
+ /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
+ /// exported automatically whenever `cargo test` is run.
+ /// In that case, there is no need to manually call this function.
+ fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError>
+ where
+ Self: 'static,
+ {
+ export::export_all_into::(out_dir)
}
/// Manually generate bindings for this type, returning a [`String`].
- /// This function does not format the output, even if the `format` feature is enabled.
+ /// This function does not format the output, even if the `format` feature is enabled. TODO
+ ///
+ /// # Automatic Exporting
+ /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
+ /// exported automatically whenever `cargo test` is run.
+ /// In that case, there is no need to manually call this function.
fn export_to_string() -> Result
where
Self: 'static,
{
- export::export_type_to_string::()
+ export::export_to_string::()
}
- /// Returns the output path to where `T` should be exported.
+ /// Returns the output path to where `T` should be exported.
+ /// The returned path does _not_ include the base directory from `TS_RS_EXPORT_DIR`.
+ ///
+ /// To get the output path containing `TS_RS_EXPORT_DIR`, use [`TS::default_output_path`].
///
/// When deriving `TS`, the output path can be altered using `#[ts(export_to = "...")]`.
/// See the documentation of [`TS`] for more details.
- ///
+ ///
/// The output of this function depends on the environment variable `TS_RS_EXPORT_DIR`, which is
/// used as base directory. If it is not set, `./bindings` is used as default directory.
///
/// If `T` cannot be exported (e.g because it's a primitive type), this function will return
/// `None`.
- fn output_path() -> Option {
+ fn output_path() -> Option<&'static Path> {
None
}
+
+ /// Returns the output path to where `T` should be exported.
+ ///
+ /// The output of this function depends on the environment variable `TS_RS_EXPORT_DIR`, which is
+ /// used as base directory. If it is not set, `./bindings` is used as default directory.
+ ///
+ /// To get the output path relative to `TS_RS_EXPORT_DIR` and without reading the environment
+ /// variable, use [`TS::output_path`].
+ ///
+ /// When deriving `TS`, the output path can be altered using `#[ts(export_to = "...")]`.
+ /// See the documentation of [`TS`] for more details.
+ ///
+ /// If `T` cannot be exported (e.g because it's a primitive type), this function will return
+ /// `None`.
+ fn default_output_path() -> Option {
+ Some(export::default_out_dir().join(Self::output_path()?))
+ }
}
/// A typescript type which is depended upon by other types.
@@ -436,8 +586,9 @@ pub struct Dependency {
/// Name of the type in TypeScript
pub ts_name: String,
/// Path to where the type would be exported. By default a filename is derived from the types
- /// name, which can be customized with `#[ts(export_to = "..")]`.
- pub exported_to: String,
+ /// name, which can be customized with `#[ts(export_to = "..")]`.
+ /// This path does _not_ include a base directory.
+ pub output_path: &'static Path,
}
impl Dependency {
@@ -445,11 +596,11 @@ impl Dependency {
/// If `T` is not exportable (meaning `T::EXPORT_TO` is `None`), this function will return
/// `None`
pub fn from_ty() -> Option {
- let exported_to = T::output_path()?.to_str()?.to_owned();
+ let output_path = T::output_path()?;
Some(Dependency {
type_id: TypeId::of::(),
ts_name: T::ident(),
- exported_to,
+ output_path,
})
}
}
@@ -543,8 +694,9 @@ macro_rules! impl_shadow {
{
<$s>::generics()
}
- fn decl() -> String { panic!("{} cannot be declared", Self::name()) }
- fn decl_concrete() -> String { panic!("{} cannot be declared", Self::name()) }
+ fn decl() -> String { <$s>::decl() }
+ fn decl_concrete() -> String { <$s>::decl_concrete() }
+ fn output_path() -> Option<&'static std::path::Path> { <$s>::output_path() }
}
};
}
@@ -724,15 +876,15 @@ impl TS for HashMap {
type WithoutGenerics = HashMap;
fn ident() -> String {
- "Record".to_owned()
+ panic!()
}
fn name() -> String {
- format!("Record<{}, {}>", K::name(), V::name())
+ format!("{{ [key: {}]: {} }}", K::name(), V::name())
}
fn inline() -> String {
- format!("Record<{}, {}>", K::inline(), V::inline())
+ format!("{{ [key: {}]: {} }}", K::inline(), V::inline())
}
fn dependency_types() -> impl TypeList
@@ -868,8 +1020,11 @@ impl_primitives! {
Ipv4Addr, Ipv6Addr, IpAddr, SocketAddrV4, SocketAddrV6, SocketAddr => "string",
() => "null"
}
+
#[rustfmt::skip]
pub(crate) use impl_primitives;
+#[rustfmt::skip]
+pub(crate) use impl_shadow;
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
diff --git a/ts-rs/src/serde_json.rs b/ts-rs/src/serde_json.rs
new file mode 100644
index 000000000..a5e63ed18
--- /dev/null
+++ b/ts-rs/src/serde_json.rs
@@ -0,0 +1,21 @@
+use std::collections::HashMap;
+
+use super::{impl_primitives, impl_shadow, TS};
+
+#[derive(TS)]
+#[ts(
+ crate = "crate",
+ rename = "JsonValue",
+ untagged,
+ export_to = "serde_json/"
+)]
+pub enum TsJsonValue {
+ Number(i32),
+ String(String),
+ Array(Vec),
+ Object(HashMap),
+}
+
+impl_shadow!(as TsJsonValue: impl TS for serde_json::Value);
+impl_primitives!(serde_json::Number => "number");
+impl_shadow!(as HashMap: impl TS for serde_json::Map);
diff --git a/ts-rs/src/typelist.rs b/ts-rs/src/typelist.rs
index c54fdf2ba..123bf521f 100644
--- a/ts-rs/src/typelist.rs
+++ b/ts-rs/src/typelist.rs
@@ -1,11 +1,11 @@
-//! A simple zero-sized collection of types.
+//! A simple zero-sized collection of types.
use std::{any::TypeId, marker::PhantomData};
use crate::TS;
/// A visitor used to iterate over a [`TypeList`].
-///
+///
/// Example:
/// ```
/// # use ts_rs::TS;
@@ -33,10 +33,10 @@ pub trait TypeVisitor: Sized {
}
/// A list containing types implementing `TS + 'static + ?Sized`.
-///
-/// To construct a [`TypeList`], start with the empty list, which is the unit type `()`, and
+///
+/// To construct a [`TypeList`], start with the empty list, which is the unit type `()`, and
/// repeatedly call [`TypeList::push`] or [`TypeList::extend`] on it.
-///
+///
/// Example:
/// ```
/// # use ts_rs::typelist::TypeList;
@@ -49,10 +49,10 @@ pub trait TypeVisitor: Sized {
/// .extend(unsigned)
/// }
/// ```
-///
-/// The only way to get access to the types contained in a [`TypeList`] is to iterate over it by
+///
+/// The only way to get access to the types contained in a [`TypeList`] is to iterate over it by
/// creating a visitor implementing [`TypeVisitor`] and calling [`TypeList::for_each`].
-///
+///
/// Under the hood, [`TypeList`] is recursively defined as follows:
/// - The unit type `()` is the empty [`TypeList`]
/// - For every `T: TS`, `(PhantomData,)` is a [`TypeList`]
diff --git a/ts-rs/tests/bound.rs b/ts-rs/tests/bound.rs
new file mode 100644
index 000000000..485d2def0
--- /dev/null
+++ b/ts-rs/tests/bound.rs
@@ -0,0 +1,36 @@
+#![allow(dead_code)]
+
+use ts_rs::TS;
+
+trait Driver {
+ type Info;
+}
+
+struct TsDriver;
+
+#[derive(TS)]
+struct TsInfo;
+
+impl Driver for TsDriver {
+ type Info = TsInfo;
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "bound/")]
+#[ts(concrete(D = TsDriver))]
+struct Inner {
+ info: D::Info,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "bound/")]
+#[ts(concrete(D = TsDriver), bound = "D::Info: TS")]
+struct Outer {
+ inner: Inner,
+}
+
+#[test]
+fn test_bound() {
+ assert_eq!(Outer::::decl(), "type Outer = { inner: Inner, };");
+ assert_eq!(Inner::::decl(), "type Inner = { info: TsInfo, };");
+}
diff --git a/ts-rs/tests/concrete_generic.rs b/ts-rs/tests/concrete_generic.rs
new file mode 100644
index 000000000..5d45877c3
--- /dev/null
+++ b/ts-rs/tests/concrete_generic.rs
@@ -0,0 +1,91 @@
+#![allow(unused)]
+
+mod issue_261 {
+ use ts_rs::TS;
+
+ trait Driver {
+ type Info;
+ }
+
+ struct TsDriver;
+ impl Driver for TsDriver {
+ type Info = String;
+ }
+
+ #[derive(TS)]
+ #[ts(export, export_to = "concrete_generic/issue_261/")]
+ struct OtherInfo {
+ x: i32,
+ }
+
+ #[derive(TS)]
+ #[ts(export, export_to = "concrete_generic/issue_261/")]
+ struct OtherDriver;
+ impl Driver for OtherDriver {
+ type Info = OtherInfo;
+ }
+
+ #[derive(TS)]
+ #[ts(export, export_to = "concrete_generic/issue_261/", concrete(T = TsDriver))]
+ struct Consumer1 {
+ info: T::Info,
+ }
+
+ #[derive(TS)]
+ #[ts(export, export_to = "concrete_generic/issue_261/", concrete(T = OtherDriver))]
+ struct Consumer2 {
+ info: T::Info,
+ driver: T,
+ }
+
+ #[test]
+ fn concrete_generic_param() {
+ assert_eq!(
+ Consumer1::::decl(),
+ "type Consumer1 = { info: string, };"
+ );
+ // `decl` must use the concrete generic, no matter what we pass in
+ assert_eq!(
+ Consumer1::::decl(),
+ Consumer1::::decl()
+ );
+
+ assert_eq!(
+ Consumer2::::decl_concrete(),
+ "type Consumer2 = { info: OtherInfo, driver: OtherDriver, };"
+ );
+ }
+}
+
+mod simple {
+ use ts_rs::TS;
+
+ #[derive(TS)]
+ #[ts(export, export_to = "concrete_generic/simple/")]
+ #[ts(concrete(T = i32))]
+ struct Simple {
+ t: T,
+ }
+
+ #[derive(TS)]
+ #[ts(export, export_to = "concrete_generic/simple/")]
+ struct Tuple {
+ f: Option,
+ }
+
+ #[derive(TS)]
+ #[ts(export, export_to = "concrete_generic/simple/")]
+ #[ts(concrete(T = i32))]
+ struct WithOption {
+ opt: Option,
+ }
+
+ #[test]
+ fn simple() {
+ assert_eq!(Simple::::decl(), "type Simple = { t: number, };");
+ assert_eq!(
+ WithOption::::decl(),
+ "type WithOption = { opt: number | null, };"
+ );
+ }
+}
diff --git a/ts-rs/tests/docs.rs b/ts-rs/tests/docs.rs
index 026df25f9..d989fa011 100644
--- a/ts-rs/tests/docs.rs
+++ b/ts-rs/tests/docs.rs
@@ -136,7 +136,7 @@ fn export_a() {
)
};
- let actual_content = fs::read_to_string(A::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(A::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
@@ -182,7 +182,7 @@ fn export_b() {
)
};
- let actual_content = fs::read_to_string(B::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(B::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
@@ -215,7 +215,7 @@ fn export_c() {
)
};
- let actual_content = fs::read_to_string(C::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(C::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
@@ -247,7 +247,7 @@ fn export_d() {
"export type D = null;"
)
};
- let actual_content = fs::read_to_string(D::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(D::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
@@ -280,7 +280,7 @@ fn export_e() {
)
};
- let actual_content = fs::read_to_string(E::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(E::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
@@ -328,7 +328,7 @@ fn export_f() {
)
};
- let actual_content = fs::read_to_string(F::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(F::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
@@ -376,7 +376,7 @@ fn export_g() {
)
};
- let actual_content = fs::read_to_string(G::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(G::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
diff --git a/ts-rs/tests/enum_flattening.rs b/ts-rs/tests/enum_flattening.rs
index 6a0e1f91c..b86850b3e 100644
--- a/ts-rs/tests/enum_flattening.rs
+++ b/ts-rs/tests/enum_flattening.rs
@@ -24,12 +24,26 @@ enum BarExternally {
Buz { c: String, d: Option },
}
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening/externally_tagged/")]
+struct NestedExternally {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooExternally,
+ u: u32,
+}
+
#[test]
fn externally_tagged() {
assert_eq!(
FooExternally::inline(),
r#"{ qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
- )
+ );
+ assert_eq!(
+ NestedExternally::inline(),
+ r#"{ u: number, qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
+ );
}
#[derive(TS)]
@@ -65,12 +79,25 @@ enum BarAdjecently {
},
}
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct NestedAdjecently {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooAdjecently,
+ u: u32,
+}
+
#[test]
fn adjacently_tagged() {
assert_eq!(
FooAdjecently::inline(),
r#"{ one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
- )
+ );
+ assert_eq!(
+ NestedAdjecently::inline(),
+ r#"{ u: number, one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
+ );
}
#[derive(TS)]
@@ -95,23 +122,46 @@ enum BarInternally {
Buz { c: String, d: Option },
}
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct NestedInternally {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooInternally,
+ u: u32,
+}
+
#[test]
fn internally_tagged() {
assert_eq!(
FooInternally::inline(),
r#"{ qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"#
- )
+ );
+ assert_eq!(
+ NestedInternally::inline(),
+ r#"{ u: number, qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"#
+ );
}
#[derive(TS)]
#[ts(export, export_to = "enum_flattening/untagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct FooUntagged {
+ one: u32,
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
baz: BarUntagged,
}
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct NestedUntagged {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooUntagged,
+ u: u32,
+}
+
#[derive(TS)]
#[ts(export, export_to = "enum_flattening/untagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
@@ -127,6 +177,10 @@ enum BarUntagged {
fn untagged() {
assert_eq!(
FooUntagged::inline(),
- r#"{ a: number, a2: string, } | { b: boolean, } | { c: string, }"#
- )
+ r#"{ one: number, } & ({ a: number, a2: string, } | { b: boolean, } | { c: string, })"#
+ );
+ assert_eq!(
+ NestedUntagged::inline(),
+ r#"{ u: number, one: number, } & ({ a: number, a2: string, } | { b: boolean, } | { c: string, })"#
+ );
}
diff --git a/ts-rs/tests/enum_flattening_nested.rs b/ts-rs/tests/enum_flattening_nested.rs
new file mode 100644
index 000000000..c15d690ef
--- /dev/null
+++ b/ts-rs/tests/enum_flattening_nested.rs
@@ -0,0 +1,243 @@
+#![allow(dead_code)]
+
+#[cfg(feature = "serde-compat")]
+use serde::Serialize;
+use ts_rs::TS;
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
+struct FooExternally {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ baz: BarExternally,
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
+enum BarExternally {
+ Baz { a: i32, a2: String },
+ Biz { b: bool },
+ Buz { c: String, d: Option },
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
+struct NestedExternally {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooExternally,
+ u: u32,
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
+struct NestedExternallyLonely {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooExternally,
+}
+
+#[test]
+fn externally_tagged() {
+ // Notice here that baz is the only field inside `FooExternally`, so the parenthesis
+ // aren't needed
+ assert_eq!(
+ FooExternally::inline(),
+ r#"{ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } }"#
+ );
+
+ // But when flattening, the parenthesis are needed due to type intesections
+ assert_eq!(
+ NestedExternally::inline(),
+ r#"{ u: number, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
+ );
+
+ // And here, they are, again, unecessary
+ assert_eq!(
+ NestedExternallyLonely::inline(),
+ r#"{ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } }"#
+ );
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "enum_flattening_nested/adjacently_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct FooAdjecently {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ baz: BarAdjecently,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "enum_flattening_nested/adjacently_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "stuff"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "stuff"))]
+enum BarAdjecently {
+ Baz {
+ a: i32,
+ a2: String,
+ },
+ Biz {
+ b: bool,
+ },
+
+ #[cfg_attr(feature = "serde-compat", serde(untagged))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+ Buz {
+ c: String,
+ d: Option,
+ },
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct NestedAdjecently {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooAdjecently,
+ u: u32,
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
+struct NestedAdjecentlyLonely {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooAdjecently,
+}
+
+#[test]
+fn adjacently_tagged() {
+ assert_eq!(
+ FooAdjecently::inline(),
+ r#"{ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, }"#
+ );
+
+ assert_eq!(
+ NestedAdjecently::inline(),
+ r#"{ u: number, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
+ );
+
+ assert_eq!(
+ NestedAdjecentlyLonely::inline(),
+ r#"{ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, }"#
+ );
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "enum_flattening_nested/internally_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct FooInternally {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ baz: BarInternally,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "enum_flattening_nested/internally_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
+enum BarInternally {
+ Baz { a: i32, a2: String },
+ Biz { b: bool },
+ Buz { c: String, d: Option },
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct NestedInternally {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooInternally,
+ u: u32,
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
+struct NestedInternallyLonely {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooInternally,
+}
+
+#[test]
+fn internally_tagged() {
+ assert_eq!(
+ FooInternally::inline(),
+ r#"{ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, }"#
+ );
+
+ assert_eq!(
+ NestedInternally::inline(),
+ r#"{ u: number, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"#
+ );
+
+ assert_eq!(
+ NestedInternallyLonely::inline(),
+ r#"{ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, }"#
+ );
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "enum_flattening_nested/untagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct FooUntagged {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ baz: BarUntagged,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "enum_flattening_nested/untagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(untagged))]
+#[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+enum BarUntagged {
+ Baz { a: i32, a2: String },
+ Biz { b: bool },
+ Buz { c: String },
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct NestedUntagged {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooUntagged,
+ u: u32,
+}
+
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
+struct NestedUntaggedLonely {
+ #[cfg_attr(feature = "serde-compat", serde(flatten))]
+ #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+ a: FooUntagged,
+}
+
+#[test]
+fn untagged() {
+ assert_eq!(
+ FooUntagged::inline(),
+ r#"{ a: number, a2: string, } | { b: boolean, } | { c: string, }"#
+ );
+
+ assert_eq!(
+ NestedUntagged::inline(),
+ r#"{ u: number, } & ({ a: number, a2: string, } | { b: boolean, } | { c: string, })"#
+ );
+
+ assert_eq!(
+ NestedUntaggedLonely::inline(),
+ r#"{ a: number, a2: string, } | { b: boolean, } | { c: string, }"#
+ );
+}
diff --git a/ts-rs/tests/enum_struct_rename_all.rs b/ts-rs/tests/enum_struct_rename_all.rs
index bbc798fd0..b43edcbc7 100644
--- a/ts-rs/tests/enum_struct_rename_all.rs
+++ b/ts-rs/tests/enum_struct_rename_all.rs
@@ -44,12 +44,16 @@ pub enum TaskStatus2 {
stdout: String,
stderr: String,
},
+
+ A(i32),
+ B(i32, i32),
+ C,
}
#[test]
pub fn enum_struct_rename_all_fields() {
assert_eq!(
TaskStatus2::inline(),
- r#"{ "Running": { "started-time": string, } } | { "Terminated": { status: number, stdout: string, stderr: string, } }"#
+ r#"{ "Running": { "started-time": string, } } | { "Terminated": { status: number, stdout: string, stderr: string, } } | { "A": number } | { "B": [number, number] } | "C""#
)
}
diff --git a/ts-rs/tests/export_manually.rs b/ts-rs/tests/export_manually.rs
index 8686a70a7..cdf111879 100644
--- a/ts-rs/tests/export_manually.rs
+++ b/ts-rs/tests/export_manually.rs
@@ -36,7 +36,7 @@ fn export_manually() {
)
};
- let actual_content = fs::read_to_string(User::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(User::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
@@ -57,7 +57,7 @@ fn export_manually_dir() {
)
};
- let actual_content = fs::read_to_string(UserDir::output_path().unwrap()).unwrap();
+ let actual_content = fs::read_to_string(UserDir::default_output_path().unwrap()).unwrap();
assert_eq!(actual_content, expected_content);
}
diff --git a/ts-rs/tests/generic_without_import.rs b/ts-rs/tests/generic_without_import.rs
new file mode 100644
index 000000000..d14704888
--- /dev/null
+++ b/ts-rs/tests/generic_without_import.rs
@@ -0,0 +1,6 @@
+#![allow(dead_code)]
+
+#[derive(ts_rs::TS)]
+struct Test {
+ field: T,
+}
diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs
index e9e10ddf3..fdf4ec2d1 100644
--- a/ts-rs/tests/generics.rs
+++ b/ts-rs/tests/generics.rs
@@ -85,7 +85,7 @@ fn test() {
assert_eq!(
Container::decl(),
- "type Container = { foo: Generic, bar: Array>, baz: Record>, };"
+ "type Container = { foo: Generic, bar: Array>, baz: { [key: string]: Generic }, };"
);
}
diff --git a/ts-rs/tests/hashmap.rs b/ts-rs/tests/hashmap.rs
index 99512eae4..e6070b03a 100644
--- a/ts-rs/tests/hashmap.rs
+++ b/ts-rs/tests/hashmap.rs
@@ -15,7 +15,7 @@ struct Hashes {
fn hashmap() {
assert_eq!(
Hashes::decl(),
- "type Hashes = { map: Record, set: Array, };"
+ "type Hashes = { map: { [key: string]: string }, set: Array, };"
)
}
@@ -35,7 +35,7 @@ struct HashesHasher {
fn hashmap_with_custom_hasher() {
assert_eq!(
HashesHasher::decl(),
- "type HashesHasher = { map: Record, set: Array, };"
+ "type HashesHasher = { map: { [key: string]: string }, set: Array