ts_rs_json_value/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 interface/type declarations from rust types
8//! </p>
9//!
10//! <div align="center">
11//! <!-- Github Actions -->
12//! <img src="https://img.shields.io/github/workflow/status/Aleph-Alpha/ts-rs/Test?style=flat-square" 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 = "7.0"
44//! ```
45//!
46//! ```rust
47//! use ts_rs::TS;
48//!
49//! #[derive(TS)]
50//! #[ts(export)]
51//! struct User {
52//! user_id: i32,
53//! first_name: String,
54//! last_name: String,
55//! }
56//! ```
57//! When running `cargo test`, the TypeScript bindings will be exported to the file `bindings/User.ts`.
58//!
59//! ## features
60//! - generate interface declarations from rust structs
61//! - generate union declarations from rust enums
62//! - inline types
63//! - flatten structs/interfaces
64//! - generate necessary imports when exporting to multiple files
65//! - serde compatibility
66//! - generic types
67//!
68//! ## limitations
69//! - generic fields cannot be inlined or flattened (#56)
70//! - type aliases must not alias generic types (#70)
71//!
72//! ## cargo features
73//! - `serde-compat` (default)
74//!
75//! Enable serde compatibility. See below for more info.
76//! - `format`
77//!
78//! When enabled, the generated typescript will be formatted.
79//! Currently, this sadly adds quite a bit of dependencies.
80//! - `chrono-impl`
81//!
82//! Implement `TS` for types from chrono
83//! - `bigdecimal-impl`
84//!
85//! Implement `TS` for types from bigdecimal
86//! - `url-impl`
87//!
88//! Implement `TS` for types from url
89//! - `uuid-impl`
90//!
91//! Implement `TS` for types from uuid
92//! - `bson-uuid-impl`
93//!
94//! Implement `TS` for types from bson
95//! - `bytes-impl`
96//!
97//! Implement `TS` for types from bytes
98//! - `indexmap-impl`
99//!
100//! Implement `TS` for `IndexMap` and `IndexSet` from indexmap
101//!
102//! - `ordered-float-impl`
103//!
104//! Implement `TS` for `OrderedFloat` from ordered_float
105//!
106//! - `heapless-impl`
107//!
108//! Implement `TS` for `Vec` from heapless
109//!
110//! - `serde-json-impl
111//!
112//! Implement `TS` for `Value` from serde_json
113//!
114//! - `schemars-impl`
115//!
116//! Implement `TS` for `Schema` from schemars
117//!
118//!
119//! If there's a type you're dealing with which doesn't implement `TS`, use `#[ts(type = "..")]` or open a PR.
120//!
121//! ## serde compatability
122//! With the `serde-compat` feature (enabled by default), serde attributes can be parsed for enums and structs.
123//! Supported serde attributes:
124//! - `rename`
125//! - `rename-all`
126//! - `tag`
127//! - `content`
128//! - `untagged`
129//! - `skip`
130//! - `skip_serializing`
131//! - `skip_deserializing`
132//! - `skip_serializing_if = "Option::is_none"`
133//! - `flatten`
134//! - `default`
135//!
136//! When ts-rs encounters an unsupported serde attribute, a warning is emitted.
137//!
138//! ## contributing
139//! Contributions are always welcome!
140//! Feel free to open an issue, discuss using GitHub discussions or open a PR.
141//! [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md)
142//!
143//! ## todo
144//! - [x] serde compatibility layer
145//! - [x] documentation
146//! - [x] use typescript types across files
147//! - [x] more enum representations
148//! - [x] generics
149//! - [ ] don't require `'static`
150
151use std::{
152 any::TypeId,
153 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
154 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
155 num::{
156 NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128,
157 NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize,
158 },
159 ops::{Range, RangeInclusive},
160 path::{Path, PathBuf},
161};
162
163pub use ts_rs_macros::TS;
164
165pub use crate::export::ExportError;
166
167#[cfg(feature = "chrono-impl")]
168mod chrono;
169mod export;
170
171/// A type which can be represented in TypeScript.
172/// Most of the time, you'd want to derive this trait instead of implementing it manually.
173/// ts-rs comes with implementations for all primitives, most collections, tuples,
174/// arrays and containers.
175///
176/// ### exporting
177/// Because Rusts procedural macros are evaluated before other compilation steps, TypeScript
178/// bindings cannot be exported during compile time.
179/// Bindings can be exported within a test, which ts-rs generates for you by adding `#[ts(export)]`
180/// to a type you wish to export to a file.
181/// If, for some reason, you need to do this during runtime, you can call [`TS::export`] yourself.
182///
183/// ### serde compatibility
184/// By default, the feature `serde-compat` is enabled.
185/// ts-rs then parses serde attributes and adjusts the generated typescript bindings accordingly.
186/// Not all serde attributes are supported yet - if you use an unsupported attribute, you'll see a
187/// warning.
188///
189/// ### container attributes
190/// attributes applicable for both structs and enums
191///
192/// - `#[ts(export)]`:
193/// Generates a test which will export the type, by default to `bindings/<name>.ts` when running
194/// `cargo test`
195///
196/// - `#[ts(export_to = "..")]`:
197/// Specifies where the type should be exported to. Defaults to `bindings/<name>.ts`.
198/// If the provided path ends in a trailing `/`, it is interpreted as a directory.
199/// Note that you need to add the `export` attribute as well, in order to generate a test which exports the type.
200///
201/// - `#[ts(rename = "..")]`:
202/// Sets the typescript name of the generated type
203///
204/// - `#[ts(rename_all = "..")]`:
205/// Rename all fields/variants of the type.
206/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
207///
208///
209/// ### struct field attributes
210///
211/// - `#[ts(type = "..")]`:
212/// Overrides the type used in TypeScript.
213/// This is useful when there's a type for which you cannot derive `TS`.
214///
215/// - `#[ts(rename = "..")]`:
216/// Renames this field
217///
218/// - `#[ts(inline)]`:
219/// Inlines the type of this field
220///
221/// - `#[ts(skip)]`:
222/// Skip this field
223///
224/// - `#[ts(optional)]`:
225/// Indicates the field may be omitted from the serialized struct
226///
227/// - `#[ts(flatten)]`:
228/// Flatten this field (only works if the field is a struct)
229///
230/// ### enum attributes
231///
232/// - `#[ts(tag = "..")]`:
233/// Changes the representation of the enum to store its tag in a separate field.
234/// See [the serde docs](https://serde.rs/enum-representations.html).
235///
236/// - `#[ts(content = "..")]`:
237/// Changes the representation of the enum to store its content in a separate field.
238/// See [the serde docs](https://serde.rs/enum-representations.html).
239///
240/// - `#[ts(untagged)]`:
241/// Changes the representation of the enum to not include its tag.
242/// See [the serde docs](https://serde.rs/enum-representations.html).
243///
244/// - `#[ts(rename_all = "..")]`:
245/// Rename all variants of this enum.
246/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
247///
248/// ### enum variant attributes
249///
250/// - `#[ts(rename = "..")]`:
251/// Renames this variant
252///
253/// - `#[ts(skip)]`:
254/// Skip this variant
255pub trait TS {
256 const EXPORT_TO: Option<&'static str> = None;
257
258 /// Declaration of this type, e.g. `interface User { user_id: number, ... }`.
259 /// This function will panic if the type has no declaration.
260 fn decl() -> String {
261 panic!("{} cannot be declared", Self::name());
262 }
263
264 /// Name of this type in TypeScript.
265 fn name() -> String;
266
267 /// Name of this type in TypeScript, with type arguments.
268 fn name_with_type_args(args: Vec<String>) -> String {
269 format!("{}<{}>", Self::name(), args.join(", "))
270 }
271
272 /// Formats this types definition in TypeScript, e.g `{ user_id: number }`.
273 /// This function will panic if the type cannot be inlined.
274 fn inline() -> String {
275 panic!("{} cannot be inlined", Self::name());
276 }
277
278 /// Flatten an type declaration.
279 /// This function will panic if the type cannot be flattened.
280 fn inline_flattened() -> String {
281 panic!("{} cannot be flattened", Self::name())
282 }
283
284 /// Information about types this type depends on.
285 /// This is used for resolving imports when exporting to a file.
286 fn dependencies() -> Vec<Dependency>
287 where
288 Self: 'static;
289
290 /// `true` if this is a transparent type, e.g tuples or a list.
291 /// This is used for resolving imports when using the `export!` macro.
292 fn transparent() -> bool;
293
294 /// Manually export this type to a file.
295 /// The output file can be specified by annotating the type with `#[ts(export_to = ".."]`.
296 /// By default, the filename will be derived from the types name.
297 ///
298 /// When a type is annotated with `#[ts(export)]`, it is exported automatically within a test.
299 /// This function is only usefull if you need to export the type outside of the context of a
300 /// test.
301 fn export() -> Result<(), ExportError>
302 where
303 Self: 'static,
304 {
305 export::export_type::<Self>()
306 }
307
308 /// Manually export this type to a file with a file with the specified path. This
309 /// function will ignore the `#[ts(export_to = "..)]` attribute.
310 fn export_to(path: impl AsRef<Path>) -> Result<(), ExportError>
311 where
312 Self: 'static,
313 {
314 export::export_type_to::<Self, _>(path)
315 }
316
317 /// Manually generate bindings for this type, returning a [`String`].
318 /// This function does not format the output, even if the `format` feature is enabled.
319 fn export_to_string() -> Result<String, ExportError>
320 where
321 Self: 'static,
322 {
323 export::export_type_to_string::<Self>()
324 }
325}
326
327/// A typescript type which is depended upon by other types.
328/// This information is required for generating the correct import statements.
329#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
330pub struct Dependency {
331 /// Type ID of the rust type
332 pub type_id: TypeId,
333 /// Name of the type in TypeScript
334 pub ts_name: String,
335 /// Path to where the type would be exported. By default a filename is derived from the types
336 /// name, which can be customized with `#[ts(export_to = "..")]`.
337 pub exported_to: &'static str,
338}
339
340impl Dependency {
341 /// Constructs a [`Dependency`] from the given type `T`.
342 /// If `T` is not exportable (meaning `T::EXPORT_TO` is `None`), this function will return
343 /// `None`
344 pub fn from_ty<T: TS + 'static + ?Sized>() -> Option<Self> {
345 let exported_to = T::EXPORT_TO?;
346 Some(Dependency {
347 type_id: TypeId::of::<T>(),
348 ts_name: T::name(),
349 exported_to,
350 })
351 }
352}
353
354// generate impls for primitive types
355macro_rules! impl_primitives {
356 ($($($ty:ty),* => $l:literal),*) => { $($(
357 impl TS for $ty {
358 fn name() -> String { $l.to_owned() }
359 fn name_with_type_args(args: Vec<String>) -> String {
360 assert!(args.is_empty(), "called name_with_type_args on primitive");
361 $l.to_owned()
362 }
363 fn inline() -> String { $l.to_owned() }
364 fn dependencies() -> Vec<Dependency> { vec![] }
365 fn transparent() -> bool { false }
366 }
367 )*)* };
368}
369// generate impls for tuples
370macro_rules! impl_tuples {
371 ( impl $($i:ident),* ) => {
372 impl<$($i: TS),*> TS for ($($i,)*) {
373 fn name() -> String {
374 format!("[{}]", [$($i::name()),*].join(", "))
375 }
376 fn inline() -> String {
377 format!("[{}]", [$($i::inline()),*].join(", "))
378 }
379 fn dependencies() -> Vec<Dependency>
380 where
381 Self: 'static
382 {
383 [$( Dependency::from_ty::<$i>() ),*]
384 .into_iter()
385 .flatten()
386 .collect()
387 }
388 fn transparent() -> bool { true }
389 }
390 };
391 ( $i2:ident $(, $i:ident)* ) => {
392 impl_tuples!(impl $i2 $(, $i)* );
393 impl_tuples!($($i),*);
394 };
395 () => {};
396}
397
398// generate impls for wrapper types
399macro_rules! impl_wrapper {
400 ($($t:tt)*) => {
401 $($t)* {
402 fn name() -> String { T::name() }
403 fn name_with_type_args(mut args: Vec<String>) -> String {
404 assert_eq!(args.len(), 1);
405 args.remove(0)
406 }
407 fn inline() -> String { T::inline() }
408 fn inline_flattened() -> String { T::inline_flattened() }
409 fn dependencies() -> Vec<Dependency>
410 where
411 Self: 'static
412 {
413 T::dependencies()
414 }
415 fn transparent() -> bool { T::transparent() }
416 }
417 };
418}
419
420// implement TS for the $shadow, deferring to the impl $s
421macro_rules! impl_shadow {
422 (as $s:ty: $($impl:tt)*) => {
423 $($impl)* {
424 fn name() -> String { <$s>::name() }
425 fn name_with_type_args(args: Vec<String>) -> String { <$s>::name_with_type_args(args) }
426 fn inline() -> String { <$s>::inline() }
427 fn inline_flattened() -> String { <$s>::inline_flattened() }
428 fn dependencies() -> Vec<$crate::Dependency>
429 where
430 Self: 'static
431 {
432 <$s>::dependencies()
433 }
434 fn transparent() -> bool { <$s>::transparent() }
435 }
436 };
437}
438
439impl<T: TS> TS for Option<T> {
440 fn name() -> String {
441 unreachable!();
442 }
443
444 fn name_with_type_args(args: Vec<String>) -> String {
445 assert_eq!(
446 args.len(),
447 1,
448 "called Option::name_with_type_args with {} args",
449 args.len()
450 );
451 format!("{} | null", args[0])
452 }
453
454 fn inline() -> String {
455 format!("{} | null", T::inline())
456 }
457
458 fn dependencies() -> Vec<Dependency>
459 where
460 Self: 'static,
461 {
462 [Dependency::from_ty::<T>()].into_iter().flatten().collect()
463 }
464
465 fn transparent() -> bool {
466 true
467 }
468}
469
470impl<T: TS> TS for Vec<T> {
471 fn name() -> String {
472 "Array".to_owned()
473 }
474
475 fn name_with_type_args(args: Vec<String>) -> String {
476 assert_eq!(
477 args.len(),
478 1,
479 "called Vec::name_with_type_args with {} args",
480 args.len()
481 );
482 format!("Array<{}>", args[0])
483 }
484
485 fn inline() -> String {
486 format!("Array<{}>", T::inline())
487 }
488
489 fn dependencies() -> Vec<Dependency>
490 where
491 Self: 'static,
492 {
493 [Dependency::from_ty::<T>()].into_iter().flatten().collect()
494 }
495
496 fn transparent() -> bool {
497 true
498 }
499}
500
501impl<K: TS, V: TS> TS for HashMap<K, V> {
502 fn name() -> String {
503 "Record".to_owned()
504 }
505
506 fn name_with_type_args(args: Vec<String>) -> String {
507 assert_eq!(
508 args.len(),
509 2,
510 "called HashMap::name_with_type_args with {} args",
511 args.len()
512 );
513 format!("Record<{}, {}>", args[0], args[1])
514 }
515
516 fn inline() -> String {
517 format!("Record<{}, {}>", K::inline(), V::inline())
518 }
519
520 fn dependencies() -> Vec<Dependency>
521 where
522 Self: 'static,
523 {
524 [Dependency::from_ty::<K>(), Dependency::from_ty::<V>()]
525 .into_iter()
526 .flatten()
527 .collect()
528 }
529
530 fn transparent() -> bool {
531 true
532 }
533}
534
535impl<I: TS> TS for Range<I> {
536 fn name() -> String {
537 panic!("called Range::name - Did you use a type alias?")
538 }
539
540 fn name_with_type_args(args: Vec<String>) -> String {
541 assert_eq!(
542 args.len(),
543 1,
544 "called Range::name_with_type_args with {} args",
545 args.len()
546 );
547 format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
548 }
549
550 fn dependencies() -> Vec<Dependency>
551 where
552 Self: 'static,
553 {
554 [Dependency::from_ty::<I>()].into_iter().flatten().collect()
555 }
556
557 fn transparent() -> bool {
558 true
559 }
560}
561
562impl<I: TS> TS for RangeInclusive<I> {
563 fn name() -> String {
564 panic!("called RangeInclusive::name - Did you use a type alias?")
565 }
566
567 fn name_with_type_args(args: Vec<String>) -> String {
568 assert_eq!(
569 args.len(),
570 1,
571 "called RangeInclusive::name_with_type_args with {} args",
572 args.len()
573 );
574 format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
575 }
576
577 fn dependencies() -> Vec<Dependency>
578 where
579 Self: 'static,
580 {
581 [Dependency::from_ty::<I>()].into_iter().flatten().collect()
582 }
583
584 fn transparent() -> bool {
585 true
586 }
587}
588
589impl_shadow!(as T: impl<'a, T: TS + ?Sized> TS for &T);
590impl_shadow!(as Vec<T>: impl<T: TS> TS for HashSet<T>);
591impl_shadow!(as Vec<T>: impl<T: TS> TS for BTreeSet<T>);
592impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for BTreeMap<K, V>);
593impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for [T; N]);
594
595impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);
596impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Arc<T>);
597impl_wrapper!(impl<T: TS + ?Sized> TS for std::rc::Rc<T>);
598impl_wrapper!(impl<'a, T: TS + ToOwned + ?Sized> TS for std::borrow::Cow<'a, T>);
599impl_wrapper!(impl<T: TS> TS for std::cell::Cell<T>);
600impl_wrapper!(impl<T: TS> TS for std::cell::RefCell<T>);
601impl_wrapper!(impl<T: TS> TS for std::sync::Mutex<T>);
602impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Weak<T>);
603impl_wrapper!(impl<T: TS> TS for std::marker::PhantomData<T>);
604
605impl_tuples!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
606
607#[cfg(feature = "bigdecimal-impl")]
608impl_primitives! { bigdecimal::BigDecimal => "string" }
609
610#[cfg(feature = "uuid-impl")]
611impl_primitives! { uuid::Uuid => "string" }
612
613#[cfg(feature = "url-impl")]
614impl_primitives! { url::Url => "string" }
615
616#[cfg(feature = "ordered-float-impl")]
617impl_primitives! { ordered_float::OrderedFloat<f32> => "number" }
618
619#[cfg(feature = "ordered-float-impl")]
620impl_primitives! { ordered_float::OrderedFloat<f64> => "number" }
621
622#[cfg(feature = "bson-uuid-impl")]
623impl_primitives! { bson::Uuid => "string" }
624
625#[cfg(feature = "indexmap-impl")]
626impl_shadow!(as Vec<T>: impl<T: TS> TS for indexmap::IndexSet<T>);
627
628#[cfg(feature = "indexmap-impl")]
629impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for indexmap::IndexMap<K, V>);
630
631#[cfg(feature = "heapless-impl")]
632impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for heapless::Vec<T, N>);
633
634#[cfg(feature = "serde-json-impl")]
635impl_primitives! { serde_json::Value => "string | number | boolean | null" }
636
637#[cfg(feature = "schemars-impl")]
638impl_primitives! { schemars::schema::Schema => "object" }
639
640#[cfg(feature = "bytes-impl")]
641mod bytes {
642 use super::TS;
643
644 impl_shadow!(as Vec<u8>: impl TS for bytes::Bytes);
645 impl_shadow!(as Vec<u8>: impl TS for bytes::BytesMut);
646}
647
648impl_primitives! {
649 u8, i8, NonZeroU8, NonZeroI8,
650 u16, i16, NonZeroU16, NonZeroI16,
651 u32, i32, NonZeroU32, NonZeroI32,
652 usize, isize, NonZeroUsize, NonZeroIsize, f32, f64 => "number",
653 u64, i64, NonZeroU64, NonZeroI64,
654 u128, i128, NonZeroU128, NonZeroI128 => "bigint",
655 bool => "boolean",
656 char, Path, PathBuf, String, str,
657 Ipv4Addr, Ipv6Addr, IpAddr, SocketAddrV4, SocketAddrV6, SocketAddr => "string",
658 () => "null"
659}
660#[rustfmt::skip]
661pub(crate) use impl_primitives;