use std::{
any::TypeId,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
num::{
NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128,
NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize,
},
ops::{Range, RangeInclusive},
path::{Path, PathBuf},
};
pub use ts_rs_macros::TS;
pub use crate::export::ExportError;
#[cfg(feature = "chrono-impl")]
mod chrono;
mod export;
#[cfg(feature = "jiff-impl")]
mod jiff;
#[cfg(feature = "serde-json-impl")]
mod serde_json;
#[cfg(feature = "tokio-impl")]
mod tokio;
pub trait TS {
type WithoutGenerics: TS + ?Sized;
type OptionInnerType: ?Sized;
#[doc(hidden)]
const IS_OPTION: bool = false;
#[doc(hidden)]
const IS_ENUM: bool = false;
fn docs() -> Option<String> {
None
}
fn ident(cfg: &Config) -> String {
let name = <Self as crate::TS>::name(cfg);
match name.find('<') {
Some(i) => name[..i].to_owned(),
None => name,
}
}
fn decl(cfg: &Config) -> String {
panic!("{} cannot be declared", Self::name(cfg))
}
fn decl_concrete(cfg: &Config) -> String {
panic!("{} cannot be declared", Self::name(cfg))
}
fn name(cfg: &Config) -> String;
fn inline(cfg: &Config) -> String;
fn inline_flattened(cfg: &Config) -> String {
panic!("{} cannot be flattened", Self::name(cfg))
}
fn visit_dependencies(_: &mut impl TypeVisitor)
where
Self: 'static,
{
}
fn visit_generics(_: &mut impl TypeVisitor)
where
Self: 'static,
{
}
fn dependencies(cfg: &Config) -> Vec<Dependency>
where
Self: 'static,
{
struct Visit<'a>(&'a Config, &'a mut Vec<Dependency>);
impl TypeVisitor for Visit<'_> {
fn visit<T: TS + 'static + ?Sized>(&mut self) {
let Visit(cfg, deps) = self;
if let Some(dep) = Dependency::from_ty::<T>(cfg) {
deps.push(dep);
}
}
}
let mut deps: Vec<Dependency> = vec![];
Self::visit_dependencies(&mut Visit(cfg, &mut deps));
deps
}
fn export(cfg: &Config) -> Result<(), ExportError>
where
Self: 'static,
{
let relative_path = Self::output_path()
.ok_or_else(std::any::type_name::<Self>)
.map_err(ExportError::CannotBeExported)?;
let path = cfg.export_dir.join(relative_path);
export::export_to::<Self, _>(cfg, path)
}
fn export_all(cfg: &Config) -> Result<(), ExportError>
where
Self: 'static,
{
export::export_all_into::<Self>(cfg)
}
fn export_to_string(cfg: &Config) -> Result<String, ExportError>
where
Self: 'static,
{
export::export_to_string::<Self>(cfg)
}
fn output_path() -> Option<PathBuf> {
None
}
}
pub trait TypeVisitor: Sized {
fn visit<T: TS + 'static + ?Sized>(&mut self);
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct Dependency {
pub type_id: TypeId,
pub ts_name: String,
pub output_path: PathBuf,
}
impl Dependency {
pub fn from_ty<T: TS + 'static + ?Sized>(cfg: &Config) -> Option<Self> {
let output_path = <T as crate::TS>::output_path()?;
Some(Dependency {
type_id: TypeId::of::<T>(),
ts_name: <T as crate::TS>::ident(cfg),
output_path,
})
}
}
pub struct Config {
large_int_type: String,
use_v11_hashmap: bool,
export_dir: PathBuf,
import_extension: Option<String>,
array_tuple_limit: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
large_int_type: "bigint".to_owned(),
use_v11_hashmap: false,
export_dir: "./bindings".into(),
import_extension: None,
array_tuple_limit: 64,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn from_env() -> Self {
let mut cfg = Self::default();
if let Ok(ty) = std::env::var("TS_RS_LARGE_INT") {
cfg = cfg.with_large_int(ty);
}
if let Ok(dir) = std::env::var("TS_RS_EXPORT_DIR") {
cfg = cfg.with_out_dir(dir);
}
if let Ok(ext) = std::env::var("TS_RS_IMPORT_EXTENSION") {
if !ext.trim().is_empty() {
cfg = cfg.with_import_extension(Some(ext));
}
}
#[allow(deprecated)]
if let Ok("1" | "true" | "on" | "yes") = std::env::var("TS_RS_USE_V11_HASHMAP").as_deref() {
cfg = cfg.with_v11_hashmap();
}
cfg
}
pub fn with_large_int(mut self, ty: impl Into<String>) -> Self {
self.large_int_type = ty.into();
self
}
pub fn large_int(&self) -> &str {
&self.large_int_type
}
#[deprecated = "this option is merely meant to aid migration to v12 and will be removed in a future release"]
pub fn with_v11_hashmap(mut self) -> Self {
self.use_v11_hashmap = true;
self
}
pub fn with_out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.export_dir = dir.into();
self
}
pub fn out_dir(&self) -> &Path {
&self.export_dir
}
pub fn with_import_extension(mut self, ext: Option<impl Into<String>>) -> Self {
self.import_extension = ext.map(Into::into);
self
}
pub fn import_extension(&self) -> Option<&str> {
self.import_extension.as_deref()
}
pub fn with_array_tuple_limit(mut self, limit: usize) -> Self {
self.array_tuple_limit = limit;
self
}
pub fn array_tuple_limit(&self) -> usize {
self.array_tuple_limit
}
}
#[doc(hidden)]
#[diagnostic::on_unimplemented(
message = "`#[ts(optional)]` can only be used on fields of type `Option`",
note = "`#[ts(optional)]` was used on a field of type {Self}, which is not permitted",
label = "`#[ts(optional)]` is not allowed on field of type {Self}"
)]
pub trait IsOption {
type Inner;
}
impl<T> IsOption for Option<T> {
type Inner = T;
}
macro_rules! impl_primitives {
($($($ty:ty),* => $l:expr),*) => { $($(
impl TS for $ty {
type WithoutGenerics = Self;
type OptionInnerType = Self;
fn name(_: &$crate::Config) -> String { String::from($l) }
fn inline(cfg: &$crate::Config) -> String { <Self as $crate::TS>::name(cfg) }
}
)*)* };
}
macro_rules! impl_large_integers {
($($ty:ty),*) => { $(
impl TS for $ty {
type WithoutGenerics = Self;
type OptionInnerType = Self;
fn name(cfg: &$crate::Config) -> String { cfg.large_int_type.clone() }
fn inline(cfg: &$crate::Config) -> String { <Self as $crate::TS>::name(cfg) }
}
)* };
}
macro_rules! impl_tuples {
( impl $($i:ident),* ) => {
impl<$($i: TS),*> TS for ($($i,)*) {
type WithoutGenerics = (Dummy, );
type OptionInnerType = Self;
fn name(cfg: &$crate::Config) -> String {
format!("[{}]", [$(<$i as $crate::TS>::name(cfg)),*].join(", "))
}
fn inline(_: &$crate::Config) -> String {
panic!("tuple cannot be inlined!");
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static
{
$(
v.visit::<$i>();
<$i as $crate::TS>::visit_generics(v);
)*
}
fn inline_flattened(_: &$crate::Config) -> String { panic!("tuple cannot be flattened") }
fn decl(_: &$crate::Config) -> String { panic!("tuple cannot be declared") }
fn decl_concrete(_: &$crate::Config) -> String { panic!("tuple cannot be declared") }
}
};
( $i2:ident $(, $i:ident)* ) => {
impl_tuples!(impl $i2 $(, $i)* );
impl_tuples!($($i),*);
};
() => {};
}
macro_rules! impl_wrapper {
($($t:tt)*) => {
$($t)* {
type WithoutGenerics = Self;
type OptionInnerType = Self;
fn name(cfg: &$crate::Config) -> String { <T as $crate::TS>::name(cfg) }
fn inline(cfg: &$crate::Config) -> String { <T as $crate::TS>::inline(cfg) }
fn inline_flattened(cfg: &$crate::Config) -> String { <T as $crate::TS>::inline_flattened(cfg) }
fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as $crate::TS>::visit_dependencies(v);
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as $crate::TS>::visit_generics(v);
v.visit::<T>();
}
fn decl(_: &$crate::Config) -> String { panic!("wrapper type cannot be declared") }
fn decl_concrete(_: &$crate::Config) -> String { panic!("wrapper type cannot be declared") }
}
};
}
macro_rules! impl_shadow {
(as $s:ty: $($impl:tt)*) => {
$($impl)* {
type WithoutGenerics = <$s as $crate::TS>::WithoutGenerics;
type OptionInnerType = <$s as $crate::TS>::OptionInnerType;
fn ident(cfg: &$crate::Config) -> String { <$s as $crate::TS>::ident(cfg) }
fn name(cfg: &$crate::Config) -> String { <$s as $crate::TS>::name(cfg) }
fn inline(cfg: &$crate::Config) -> String { <$s as $crate::TS>::inline(cfg) }
fn inline_flattened(cfg: &$crate::Config) -> String { <$s as $crate::TS>::inline_flattened(cfg) }
fn visit_dependencies(v: &mut impl $crate::TypeVisitor)
where
Self: 'static,
{
<$s as $crate::TS>::visit_dependencies(v);
}
fn visit_generics(v: &mut impl $crate::TypeVisitor)
where
Self: 'static,
{
<$s as $crate::TS>::visit_generics(v);
}
fn decl(cfg: &$crate::Config) -> String { <$s as $crate::TS>::decl(cfg) }
fn decl_concrete(cfg: &$crate::Config) -> String { <$s as $crate::TS>::decl_concrete(cfg) }
fn output_path() -> Option<std::path::PathBuf> { <$s as $crate::TS>::output_path() }
}
};
}
impl<T: TS> TS for Option<T> {
type WithoutGenerics = Self;
type OptionInnerType = T;
const IS_OPTION: bool = true;
fn name(cfg: &Config) -> String {
format!("{} | null", T::name(cfg))
}
fn inline(cfg: &Config) -> String {
format!("{} | null", T::inline(cfg))
}
fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_dependencies(v);
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_generics(v);
v.visit::<T>();
}
}
impl<T: TS, E: TS> TS for Result<T, E> {
type WithoutGenerics = Result<Dummy, Dummy>;
type OptionInnerType = Self;
fn name(cfg: &Config) -> String {
format!("{{ Ok : {} }} | {{ Err : {} }}", T::name(cfg), E::name(cfg))
}
fn inline(cfg: &Config) -> String {
format!(
"{{ Ok : {} }} | {{ Err : {} }}",
T::inline(cfg),
E::inline(cfg)
)
}
fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_dependencies(v);
<E as crate::TS>::visit_dependencies(v);
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_generics(v);
v.visit::<T>();
<E as crate::TS>::visit_generics(v);
v.visit::<E>();
}
}
impl<T: TS> TS for Vec<T> {
type WithoutGenerics = Vec<Dummy>;
type OptionInnerType = Self;
fn ident(_: &Config) -> String {
"Array".to_owned()
}
fn name(cfg: &Config) -> String {
format!("Array<{}>", T::name(cfg))
}
fn inline(cfg: &Config) -> String {
format!("Array<{}>", T::inline(cfg))
}
fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_dependencies(v);
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_generics(v);
v.visit::<T>();
}
}
impl<T: TS, const N: usize> TS for [T; N] {
type WithoutGenerics = [Dummy; N];
type OptionInnerType = Self;
fn name(cfg: &Config) -> String {
if N > cfg.array_tuple_limit() {
return <Vec<T> as crate::TS>::name(cfg);
}
format!(
"[{}]",
(0..N)
.map(|_| T::name(cfg))
.collect::<Box<[_]>>()
.join(", ")
)
}
fn inline(cfg: &Config) -> String {
if N > cfg.array_tuple_limit() {
return <Vec<T> as crate::TS>::inline(cfg);
}
format!(
"[{}]",
(0..N)
.map(|_| T::inline(cfg))
.collect::<Box<[_]>>()
.join(", ")
)
}
fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_dependencies(v);
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static,
{
<T as crate::TS>::visit_generics(v);
v.visit::<T>();
}
}
impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
type WithoutGenerics = HashMap<Dummy, Dummy>;
type OptionInnerType = Self;
fn ident(_: &Config) -> String {
panic!()
}
fn name(cfg: &Config) -> String {
let optional = K::IS_ENUM || cfg.use_v11_hashmap;
format!(
"{{ [key in {}]{}: {} }}",
K::name(cfg),
if optional { "?" } else { "" },
V::name(cfg),
)
}
fn inline(cfg: &Config) -> String {
let optional = K::IS_ENUM || cfg.use_v11_hashmap;
format!(
"{{ [key in {}]{}: {} }}",
K::inline(cfg),
if optional { "?" } else { "" },
V::inline(cfg),
)
}
fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
{
K::visit_dependencies(v);
V::visit_dependencies(v);
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static,
{
K::visit_generics(v);
v.visit::<K>();
V::visit_generics(v);
v.visit::<V>();
}
fn inline_flattened(cfg: &Config) -> String {
format!("({})", Self::inline(cfg))
}
}
impl<I: TS> TS for Range<I> {
type WithoutGenerics = Range<Dummy>;
type OptionInnerType = Self;
fn name(cfg: &Config) -> String {
let name = I::name(cfg);
format!("{{ start: {name}, end: {name}, }}")
}
fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
{
I::visit_dependencies(v);
}
fn visit_generics(v: &mut impl TypeVisitor)
where
Self: 'static,
{
I::visit_generics(v);
v.visit::<I>();
}
fn inline(cfg: &Config) -> String {
panic!("{} cannot be inlined", Self::name(cfg))
}
}
impl_shadow!(as Range<I>: impl<I: TS> TS for RangeInclusive<I>);
impl_shadow!(as Vec<T>: impl<T: TS, H> TS for HashSet<T, H>);
impl_shadow!(as Vec<T>: impl<T: TS> TS for BTreeSet<T>);
impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for BTreeMap<K, V>);
impl_shadow!(as Vec<T>: impl<T: TS> TS for [T]);
impl_wrapper!(impl<T: TS + ?Sized> TS for &T);
impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);
impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Arc<T>);
impl_wrapper!(impl<T: TS + ?Sized> TS for std::rc::Rc<T>);
impl_wrapper!(impl<'a, T: TS + ToOwned + ?Sized> TS for std::borrow::Cow<'a, T>);
impl_wrapper!(impl<T: TS> TS for std::cell::Cell<T>);
impl_wrapper!(impl<T: TS> TS for std::cell::RefCell<T>);
impl_wrapper!(impl<T: TS> TS for std::sync::Mutex<T>);
impl_wrapper!(impl<T: TS> TS for std::sync::RwLock<T>);
impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Weak<T>);
impl_wrapper!(impl<T: TS> TS for std::marker::PhantomData<T>);
impl_tuples!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
#[cfg(feature = "bigdecimal-impl")]
impl_primitives! { bigdecimal::BigDecimal => "string" }
#[cfg(feature = "smol_str-impl")]
impl_primitives! { smol_str::SmolStr => "string" }
#[cfg(feature = "uuid-impl")]
impl_primitives! { uuid::Uuid => "string" }
#[cfg(feature = "url-impl")]
impl_primitives! { url::Url => "string" }
#[cfg(feature = "ordered-float-impl")]
impl_primitives! { ordered_float::OrderedFloat<f32> => "number" }
#[cfg(feature = "ordered-float-impl")]
impl_primitives! { ordered_float::OrderedFloat<f64> => "number" }
#[cfg(feature = "bson-uuid-impl")]
impl_primitives! { bson::oid::ObjectId => "string" }
#[cfg(feature = "bson-uuid-impl")]
impl_primitives! { bson::Uuid => "string" }
#[cfg(feature = "indexmap-impl")]
impl_shadow!(as Vec<T>: impl<T: TS> TS for indexmap::IndexSet<T>);
#[cfg(feature = "indexmap-impl")]
impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for indexmap::IndexMap<K, V>);
#[cfg(feature = "heapless-impl")]
impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for heapless::Vec<T, N>);
#[cfg(feature = "arrayvec-impl")]
impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for arrayvec::ArrayVec<T, N>);
#[cfg(feature = "arrayvec-impl")]
impl_shadow!(as String: impl<const N: usize> TS for arrayvec::ArrayString<N>);
#[cfg(feature = "semver-impl")]
impl_primitives! { semver::Version => "string" }
#[cfg(feature = "bytes-impl")]
mod bytes {
use super::TS;
impl_shadow!(as Vec<u8>: impl TS for bytes::Bytes);
impl_shadow!(as Vec<u8>: impl TS for bytes::BytesMut);
}
impl_primitives! {
u8, i8, NonZeroU8, NonZeroI8,
u16, i16, NonZeroU16, NonZeroI16,
u32, i32, NonZeroU32, NonZeroI32,
usize, isize, NonZeroUsize, NonZeroIsize, f32, f64 => "number",
bool => "boolean",
char, Path, PathBuf, String, str,
Ipv4Addr, Ipv6Addr, IpAddr, SocketAddrV4, SocketAddrV6, SocketAddr => "string",
() => "null"
}
impl_large_integers! {
u64, i64, NonZeroU64, NonZeroI64,
u128, i128, NonZeroU128, NonZeroI128
}
#[allow(unused_imports)]
#[rustfmt::skip]
pub(crate) use impl_primitives;
#[allow(unused_imports)]
#[rustfmt::skip]
pub(crate) use impl_shadow;
#[allow(unused_imports)]
#[rustfmt::skip]
pub(crate) use impl_wrapper;
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct Dummy;
impl std::fmt::Display for Dummy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl TS for Dummy {
type WithoutGenerics = Self;
type OptionInnerType = Self;
fn name(_: &Config) -> String {
"Dummy".to_owned()
}
fn inline(cfg: &Config) -> String {
panic!("{} cannot be inlined", Self::name(cfg))
}
}
#[doc(hidden)]
pub fn format_docs(docs: &[&str]) -> String {
match docs {
[] => String::new(),
[doc] if doc.contains('\n') => format!("/**{doc}*/\n"),
_ => {
let mut buffer = String::from("/**\n");
let mut lines = docs.iter().peekable();
while let Some(line) = lines.next() {
buffer.push_str(" *");
buffer.push_str(line);
if lines.peek().is_some() {
buffer.push('\n');
}
}
buffer.push_str("\n */\n");
buffer
}
}
}