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}