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;