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