Skip to main content

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