diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a2daab8e..4011b70ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,33 @@ on: [push, pull_request] name: Test jobs: + e2e-dependencies: + name: Run 'dependencies' end-to-end test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: dependencies e2e test + working-directory: e2e/dependencies/consumer + run: | + cargo t + tsc bindings/* --noEmit + e2e-workspace: + name: Run 'workspace' end-to-end test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: workspace e2e test + working-directory: e2e/workspace + run: | + cargo t + tsc parent/bindings/* --noEmit + readme-up-to-date: name: Check that README.md is up-to-date runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 1b8a7551f..999300305 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +target/ Cargo.lock /.idea *.ts diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000..29912729b --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,8 @@ +# e2e tests +### [dependencies](./dependencies) +A user creates the crate `consumer`, which depends on some crate `dependency1`, which possibly lives on [crates.io](https://crates.io). +If a struct in `consumer` contains a type from `dependency1`, it should be exported as well. + +### [workspace](./workspace) +A user creates a workspace, containing `crate1`, `crate2`, and `parent`. +`crate1` and `crate2` are independent, but `parent` depends on both `crate1` and `crate2`. diff --git a/e2e/dependencies/consumer/Cargo.toml b/e2e/dependencies/consumer/Cargo.toml new file mode 100644 index 000000000..f2c0daec6 --- /dev/null +++ b/e2e/dependencies/consumer/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "consumer" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +ts-rs = { path = "../../../ts-rs" } +dependency1 = { path = "../dependency1" } \ No newline at end of file diff --git a/e2e/dependencies/consumer/src/main.rs b/e2e/dependencies/consumer/src/main.rs new file mode 100644 index 000000000..5a79fbac4 --- /dev/null +++ b/e2e/dependencies/consumer/src/main.rs @@ -0,0 +1,10 @@ +use ts_rs::TS; +use dependency1::LibraryType; + +#[derive(TS)] +#[ts(export)] +struct ConsumerType { + pub ty: LibraryType +} + +fn main() {} \ No newline at end of file diff --git a/e2e/dependencies/dependency1/Cargo.toml b/e2e/dependencies/dependency1/Cargo.toml new file mode 100644 index 000000000..5f82773f4 --- /dev/null +++ b/e2e/dependencies/dependency1/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dependency1" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +ts-rs = { path = "../../../ts-rs" } \ No newline at end of file diff --git a/e2e/dependencies/dependency1/src/lib.rs b/e2e/dependencies/dependency1/src/lib.rs new file mode 100644 index 000000000..aade3eb50 --- /dev/null +++ b/e2e/dependencies/dependency1/src/lib.rs @@ -0,0 +1,6 @@ +use ts_rs::TS; + +#[derive(TS)] +pub struct LibraryType { + pub a: i32 +} \ No newline at end of file diff --git a/e2e/workspace/Cargo.toml b/e2e/workspace/Cargo.toml new file mode 100644 index 000000000..d06ca9311 --- /dev/null +++ b/e2e/workspace/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crate1", "crate2", "parent"] \ No newline at end of file diff --git a/e2e/workspace/crate1/Cargo.toml b/e2e/workspace/crate1/Cargo.toml new file mode 100644 index 000000000..3545a8fa0 --- /dev/null +++ b/e2e/workspace/crate1/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "crate1" +version = "0.1.0" +edition = "2021" + +[dependencies] +ts-rs = { path = "../../../ts-rs" } \ No newline at end of file diff --git a/e2e/workspace/crate1/src/lib.rs b/e2e/workspace/crate1/src/lib.rs new file mode 100644 index 000000000..afa054635 --- /dev/null +++ b/e2e/workspace/crate1/src/lib.rs @@ -0,0 +1,6 @@ +use ts_rs::TS; + +#[derive(TS)] +pub struct Crate1 { + pub x: [[[i32; 128]; 128]; 128], +} \ No newline at end of file diff --git a/e2e/workspace/crate2/Cargo.toml b/e2e/workspace/crate2/Cargo.toml new file mode 100644 index 000000000..5b6f0b260 --- /dev/null +++ b/e2e/workspace/crate2/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "crate2" +version = "0.1.0" +edition = "2021" + +[dependencies] +ts-rs = { path = "../../../ts-rs" } diff --git a/e2e/workspace/crate2/src/lib.rs b/e2e/workspace/crate2/src/lib.rs new file mode 100644 index 000000000..9617cfbf1 --- /dev/null +++ b/e2e/workspace/crate2/src/lib.rs @@ -0,0 +1,6 @@ +use ts_rs::TS; + +#[derive(TS)] +pub struct Crate2 { + pub x: [[[i32; 128]; 128]; 128], +} diff --git a/e2e/workspace/parent/Cargo.toml b/e2e/workspace/parent/Cargo.toml new file mode 100644 index 000000000..72acf9990 --- /dev/null +++ b/e2e/workspace/parent/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "parent" +version = "0.1.0" +edition = "2021" + +[dependencies] +ts-rs = { path = "../../../ts-rs" } +crate1 = { path = "../crate1" } +crate2 = { path = "../crate2" } diff --git a/e2e/workspace/parent/src/main.rs b/e2e/workspace/parent/src/main.rs new file mode 100644 index 000000000..12c9f4e13 --- /dev/null +++ b/e2e/workspace/parent/src/main.rs @@ -0,0 +1,98 @@ +use ts_rs::TS; +use crate1::Crate1; +use crate2::Crate2; + +fn main() { + println!("Hello, world!"); +} + +#[derive(TS)] +#[ts(export)] +pub struct Parent { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent2 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent3 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent4 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent5 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent6 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent7 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent8 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent9 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent10 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent11 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent12 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent13 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent14 { + pub crate1: Crate1, + pub crate2: Crate2, +} +#[derive(TS)] +#[ts(export)] +pub struct Parent15 { + pub crate1: Crate1, + pub crate2: Crate2, +} diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index 3625224e6..5897dae82 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -1,5 +1,5 @@ -use syn::{Attribute, Ident, Result}; use syn::spanned::Spanned; +use syn::{Attribute, Ident, Result}; use crate::utils::parse_attrs; @@ -60,7 +60,7 @@ impl FieldAttr { self.skip = self.skip || skip; self.optional = Optional { optional: self.optional.optional || optional, - nullable: self.optional.nullable || nullable + nullable: self.optional.nullable || nullable, }; self.flatten |= flatten; } diff --git a/macros/src/deps.rs b/macros/src/deps.rs index b28ce8b5f..b99605656 100644 --- a/macros/src/deps.rs +++ b/macros/src/deps.rs @@ -9,39 +9,26 @@ impl Dependencies { /// Adds all dependencies from the given type pub fn append_from(&mut self, ty: &Type) { self.0 - .push(quote!(dependencies.append(&mut <#ty as ts_rs::TS>::dependencies());)); + .push(quote![.extend(<#ty as ts_rs::TS>::dependency_types())]); } /// Adds the given type if it's *not* transparent. /// If it is, all it's child dependencies are added instead. pub fn push_or_append_from(&mut self, ty: &Type) { - self.0.push(quote! { - if <#ty as ts_rs::TS>::transparent() { - dependencies.append(&mut <#ty as ts_rs::TS>::dependencies()); - } else { - if let Some(dep) = ts_rs::Dependency::from_ty::<#ty>() { - dependencies.push(dep); - } - } - }); + self.0.push(quote![.push::<#ty>()]); } pub fn append(&mut self, other: Dependencies) { - self.0.push(quote! { - dependencies.append(&mut #other); - }) + self.0.push(quote![.extend(#other)]); } } impl ToTokens for Dependencies { fn to_tokens(&self, tokens: &mut TokenStream) { - let dependencies = &self.0; - tokens.extend(quote! { - { - let mut dependencies = Vec::new(); - #( #dependencies )* - dependencies - } - }) + let lines = &self.0; + tokens.extend(quote![{ + use ts_rs::typelist::TypeList; + ()#(#lines)* + }]) } } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 9b6639e96..452d9922e 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -95,12 +95,15 @@ impl DerivedTS { #inline } #inline_flattened - fn dependencies() -> Vec + + #[allow(clippy::unused_unit)] + fn dependency_types() -> impl ts_rs::typelist::TypeList where Self: 'static, { #dependencies } + fn transparent() -> bool { false } diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 8978fa52d..a4738223e 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -115,7 +115,9 @@ fn format_variant( let ty = match (type_override, type_as) { (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"), (Some(type_override), None) => quote! { #type_override }, - (None, Some(type_as)) => format_type(&syn::parse_str::(&type_as)?, dependencies, generics), + (None, Some(type_as)) => { + format_type(&syn::parse_str::(&type_as)?, dependencies, generics) + } (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), }; @@ -159,7 +161,9 @@ fn format_variant( let ty = match (type_override, type_as) { (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"), (Some(type_override), None) => quote! { #type_override }, - (None, Some(type_as)) => format_type(&syn::parse_str::(&type_as)?, dependencies, generics), + (None, Some(type_as)) => { + format_type(&syn::parse_str::(&type_as)?, dependencies, generics) + } (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), }; diff --git a/macros/src/types/generics.rs b/macros/src/types/generics.rs index 307542ff7..7da2c48ba 100644 --- a/macros/src/types/generics.rs +++ b/macros/src/types/generics.rs @@ -83,7 +83,7 @@ pub fn format_type(ty: &Type, dependencies: &mut Dependencies, generics: &Generi // be cause the T in `[T; N]` is technically not a generic Type::Array(type_array) => { let formatted = format_type(&type_array.elem, dependencies, generics); - return quote!(<#type_array>::name_with_type_args(vec![#formatted])) + return quote!(<#type_array>::name_with_type_args(vec![#formatted])); } // The field is a slice (`[T]`) so it technically doesn't have a // generic argument. Therefore, we handle it explicitly here like a `Vec` diff --git a/ts-rs/src/chrono.rs b/ts-rs/src/chrono.rs index 092626e03..7aee2a9b2 100644 --- a/ts-rs/src/chrono.rs +++ b/ts-rs/src/chrono.rs @@ -7,14 +7,12 @@ use chrono::{ }; use super::{impl_primitives, TS}; -use crate::Dependency; macro_rules! impl_dummy { ($($t:ty),*) => {$( impl TS for $t { fn name() -> String { String::new() } fn inline() -> String { String::new() } - fn dependencies() -> Vec { vec![] } fn transparent() -> bool { false } } )*}; @@ -33,9 +31,6 @@ impl TS for DateTime { fn inline() -> String { "string".to_owned() } - fn dependencies() -> Vec { - vec![] - } fn transparent() -> bool { false } @@ -51,9 +46,6 @@ impl TS for Date { fn inline() -> String { "string".to_owned() } - fn dependencies() -> Vec { - vec![] - } fn transparent() -> bool { false } diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index fdaf85164..626fed9f4 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -4,8 +4,10 @@ use std::{ fmt::Write, path::{Component, Path, PathBuf}, }; +use std::sync::Mutex; use thiserror::Error; + use ExportError::*; use crate::TS; @@ -16,7 +18,7 @@ const NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Ale #[derive(Error, Debug)] pub enum ExportError { #[error("this type cannot be exported")] - CannotBeExported, + CannotBeExported(&'static str), #[cfg(feature = "format")] #[error("an error occurred while formatting the generated typescript output")] Formatting(String), @@ -26,6 +28,67 @@ pub enum ExportError { ManifestDirNotSet, } +pub(crate) use recursive_export::export_type_with_dependencies; +mod recursive_export { + use std::{any::TypeId, collections::HashSet}; + + use super::export_type; + use crate::{ + typelist::{TypeList, TypeVisitor}, + ExportError, TS, + }; + + struct Visit<'a> { + seen: &'a mut HashSet, + error: Option, + } + + impl<'a> TypeVisitor for Visit<'a> { + fn visit(&mut self) { + // if an error occurred previously, or the type cannot be exported (it's a primitive), + // we return + if self.error.is_some() || T::EXPORT_TO.is_none() { + return; + } + + self.error = export_recursive::(self.seen).err(); + } + } + + /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute. + /// Additionally, all dependencies of `T` will be exported as well. + /// + /// TODO: This might cause a race condition: + /// If two types `A` and `B` are `#[ts(export)]` and depend on type `C`, + /// then both tests for exporting `A` and `B` will try to write `C` to `C.ts`. + /// Since rust, by default, executes tests in paralell, this might cause `C.ts` to be corrupted. + pub(crate) fn export_type_with_dependencies( + ) -> Result<(), ExportError> { + let mut seen = HashSet::new(); + export_recursive::(&mut seen) + } + + // exports T, then recursively calls itself with all of its dependencies + fn export_recursive( + seen: &mut HashSet, + ) -> Result<(), ExportError> { + if !seen.insert(TypeId::of::()) { + return Ok(()); + } + + export_type::()?; + + let mut visitor = Visit { seen, error: None }; + T::dependency_types().for_each(&mut visitor); + + if let Some(e) = visitor.error { + Err(e) + } else { + Ok(()) + } + } +} + /// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute pub(crate) fn export_type() -> Result<(), ExportError> { let path = output_path::()?; @@ -36,6 +99,11 @@ pub(crate) fn export_type() -> Result<(), ExportError> pub(crate) fn export_type_to>( path: P, ) -> Result<(), ExportError> { + // Lock to make sure only one file will be written at a time. + // In the future, it might make sense to replace this with something more clever to only prevent + // two threads from writing the **same** file concurrently. + static FILE_LOCK: Mutex<()> = Mutex::new(()); + #[allow(unused_mut)] let mut buffer = export_type_to_string::()?; @@ -55,7 +123,9 @@ pub(crate) fn export_type_to>( if let Some(parent) = path.as_ref().parent() { std::fs::create_dir_all(parent)?; } + let lock = FILE_LOCK.lock().unwrap(); std::fs::write(path.as_ref(), buffer)?; + drop(lock); Ok(()) } @@ -72,7 +142,7 @@ pub(crate) fn export_type_to_string() -> Result() -> Result { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| ManifestDirNotSet)?; let manifest_dir = Path::new(&manifest_dir); - let path = PathBuf::from(T::EXPORT_TO.ok_or(CannotBeExported)?); + let path = PathBuf::from(T::EXPORT_TO.ok_or(CannotBeExported(std::any::type_name::()))?); Ok(manifest_dir.join(path)) } @@ -84,7 +154,7 @@ fn generate_decl(out: &mut String) { /// Push an import statement for all dependencies of `T` fn generate_imports(out: &mut String) -> Result<(), ExportError> { - let path = Path::new(T::EXPORT_TO.ok_or(ExportError::CannotBeExported)?); + let path = Path::new(T::EXPORT_TO.ok_or(CannotBeExported(std::any::type_name::()))?); let deps = T::dependencies(); let deduplicated_deps = deps diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index d3a1f41ab..5a3d3a205 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -165,6 +165,8 @@ use std::{ path::{Path, PathBuf}, }; +use crate::typelist::TypeList; + pub use ts_rs_macros::TS; pub use crate::export::ExportError; @@ -172,6 +174,7 @@ pub use crate::export::ExportError; #[cfg(feature = "chrono-impl")] mod chrono; mod export; +pub mod typelist; /// A type which can be represented in TypeScript. /// Most of the time, you'd want to derive this trait instead of implementing it manually. @@ -289,13 +292,33 @@ pub trait TS { panic!("{} cannot be flattened", Self::name()) } - /// Information about types this type depends on. - /// This is used for resolving imports when exporting to a file. + fn dependency_types() -> impl TypeList + where + Self: 'static, + { + } + fn dependencies() -> Vec where - Self: 'static; + Self: 'static, + { + use crate::typelist::TypeVisitor; + + let mut deps: Vec = vec![]; + struct Visit<'a>(&'a mut Vec); + impl<'a> TypeVisitor for Visit<'a> { + fn visit(&mut self) { + if let Some(dep) = Dependency::from_ty::() { + self.0.push(dep); + } + } + } + Self::dependency_types().for_each(&mut Visit(&mut deps)); - /// `true` if this is a transparent type, e.g tuples or a list. + deps + } + + /// `true` if this is a transparent type, e.g tuples or a list. /// This is used for resolving imports when using the `export!` macro. fn transparent() -> bool; @@ -310,7 +333,7 @@ pub trait TS { where Self: 'static, { - export::export_type::() + export::export_type_with_dependencies::() } /// Manually export this type to a file with a file with the specified path. This @@ -369,7 +392,6 @@ macro_rules! impl_primitives { $l.to_owned() } fn inline() -> String { $l.to_owned() } - fn dependencies() -> Vec { vec![] } fn transparent() -> bool { false } } )*)* }; @@ -384,14 +406,11 @@ macro_rules! impl_tuples { fn inline() -> String { format!("[{}]", [$($i::inline()),*].join(", ")) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static { - [$( Dependency::from_ty::<$i>() ),*] - .into_iter() - .flatten() - .collect() + ()$(.push::<$i>())* } fn transparent() -> bool { true } } @@ -414,11 +433,11 @@ macro_rules! impl_wrapper { } fn inline() -> String { T::inline() } fn inline_flattened() -> String { T::inline_flattened() } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static { - T::dependencies() + T::dependency_types() } fn transparent() -> bool { T::transparent() } } @@ -433,11 +452,11 @@ macro_rules! impl_shadow { fn name_with_type_args(args: Vec) -> String { <$s>::name_with_type_args(args) } fn inline() -> String { <$s>::inline() } fn inline_flattened() -> String { <$s>::inline_flattened() } - fn dependencies() -> Vec<$crate::Dependency> + fn dependency_types() -> impl $crate::typelist::TypeList where Self: 'static { - <$s>::dependencies() + <$s>::dependency_types() } fn transparent() -> bool { <$s>::transparent() } } @@ -463,11 +482,11 @@ impl TS for Option { format!("{} | null", T::inline()) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static, { - [Dependency::from_ty::()].into_iter().flatten().collect() + ().push::() } fn transparent() -> bool { @@ -482,14 +501,11 @@ impl TS for Result { fn inline() -> String { format!("{{ Ok : {} }} | {{ Err : {} }}", T::inline(), E::inline()) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static, { - [Dependency::from_ty::(), Dependency::from_ty::()] - .into_iter() - .flatten() - .collect() + ().push::().push::() } fn transparent() -> bool { true @@ -505,24 +521,23 @@ impl TS for Vec { format!("Array<{}>", T::inline()) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static, { - [Dependency::from_ty::()].into_iter().flatten().collect() + ().push::() } - fn transparent() -> bool { true } } -// Arrays longer than this limit will be emmited as Array -const ARRAY_TUPLE_LIMIT: usize = 128; +// Arrays longer than this limit will be emitted as Array +const ARRAY_TUPLE_LIMIT: usize = 64; impl TS for [T; N] { fn name() -> String { if N > ARRAY_TUPLE_LIMIT { - return Vec::::name() + return Vec::::name(); } "[]".to_owned() @@ -542,7 +557,10 @@ impl TS for [T; N] { format!( "[{}]", - (0..N).map(|_| args[0].clone()).collect::>().join(", ") + (0..N) + .map(|_| args[0].clone()) + .collect::>() + .join(", ") ) } @@ -557,11 +575,11 @@ impl TS for [T; N] { ) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static, { - [Dependency::from_ty::()].into_iter().flatten().collect() + ().push::() } fn transparent() -> bool { @@ -588,14 +606,11 @@ impl TS for HashMap { format!("Record<{}, {}>", K::inline(), V::inline()) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static, { - [Dependency::from_ty::(), Dependency::from_ty::()] - .into_iter() - .flatten() - .collect() + ().push::().push::() } fn transparent() -> bool { @@ -618,11 +633,11 @@ impl TS for Range { format!("{{ start: {}, end: {}, }}", &args[0], &args[0]) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static, { - [Dependency::from_ty::()].into_iter().flatten().collect() + ().push::() } fn transparent() -> bool { @@ -645,11 +660,11 @@ impl TS for RangeInclusive { format!("{{ start: {}, end: {}, }}", &args[0], &args[0]) } - fn dependencies() -> Vec + fn dependency_types() -> impl TypeList where Self: 'static, { - [Dependency::from_ty::()].into_iter().flatten().collect() + ().push::() } fn transparent() -> bool { @@ -657,7 +672,7 @@ impl TS for RangeInclusive { } } -impl_shadow!(as T: impl<'a, T: TS + ?Sized> TS for &T); +impl_shadow!(as T: impl TS for &T); impl_shadow!(as Vec: impl TS for HashSet); impl_shadow!(as Vec: impl TS for BTreeSet); impl_shadow!(as HashMap: impl TS for BTreeMap); diff --git a/ts-rs/src/typelist.rs b/ts-rs/src/typelist.rs new file mode 100644 index 000000000..edb46f6e8 --- /dev/null +++ b/ts-rs/src/typelist.rs @@ -0,0 +1,55 @@ +use std::any::TypeId; +use std::marker::PhantomData; + +use crate::TS; + +pub trait TypeVisitor: Sized { + fn visit(&mut self); +} + +pub trait TypeList: Copy + Clone { + fn push(self) -> impl TypeList { + (self, (PhantomData::,)) + } + fn extend(self, l: impl TypeList) -> impl TypeList { + (self, l) + } + + fn contains(self) -> bool; + fn for_each(self, v: &mut impl TypeVisitor); +} + +impl TypeList for () { + fn contains(self) -> bool { + false + } + fn for_each(self, _: &mut impl TypeVisitor) {} +} + +impl TypeList for (PhantomData,) +where + T: TS + 'static + ?Sized, +{ + fn contains(self) -> bool { + TypeId::of::() == TypeId::of::() + } + + fn for_each(self, v: &mut impl TypeVisitor) { + v.visit::(); + } +} + +impl TypeList for (A, B) +where + A: TypeList, + B: TypeList, +{ + fn contains(self) -> bool { + self.0.contains::() || self.1.contains::() + } + + fn for_each(self, v: &mut impl TypeVisitor) { + self.0.for_each(v); + self.1.for_each(v); + } +} diff --git a/ts-rs/tests/arrays.rs b/ts-rs/tests/arrays.rs index f974b3830..4753b7eb7 100644 --- a/ts-rs/tests/arrays.rs +++ b/ts-rs/tests/arrays.rs @@ -13,7 +13,10 @@ fn interface() { a: [i32; 4], } - assert_eq!(Interface::inline(), "{ a: [number, number, number, number], }") + assert_eq!( + Interface::inline(), + "{ a: [number, number, number, number], }" + ) } #[test] diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index 6141005ed..7397caaed 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -171,6 +171,7 @@ fn inline() { #[test] #[ignore = "We haven't figured out how to inline generics with bounds yet"] +#[allow(unreachable_code)] fn inline_with_bounds() { todo!("FIX ME: https://github.com/Aleph-Alpha/ts-rs/issues/214"); @@ -190,12 +191,14 @@ fn inline_with_bounds() { t: Generic, } - assert_eq!(Generic::<&'static str>::decl(), "type Generic = { t: T, }"); + assert_eq!( + Generic::<&'static str>::decl(), + "type Generic = { t: T, }" + ); // ^^^^^^^^^^^^ Replace with something else assert_eq!( Container::decl(), - "type Container = { g: Generic, gi: { t: string, }, t: number, }" - // Actual output: { g: Generic, gi: { t: T, }, t: T, } + "type Container = { g: Generic, gi: { t: string, }, t: number, }" // Actual output: { g: Generic, gi: { t: T, }, t: T, } ); } @@ -209,7 +212,7 @@ fn inline_with_default() { #[derive(TS)] struct Container { g: Generic, - + #[ts(inline)] gi: Generic, @@ -217,7 +220,10 @@ fn inline_with_default() { t: Generic, } - assert_eq!(Generic::<()>::decl(), "type Generic = { t: T, }"); + assert_eq!( + Generic::<()>::decl(), + "type Generic = { t: T, }" + ); assert_eq!( Container::decl(), "type Container = { g: Generic, gi: { t: string, }, t: number, }" @@ -282,6 +288,9 @@ fn trait_bounds() { t: [T; N], } - let ty = format!("type D = {{ t: [{}], }}", "T, ".repeat(41).trim_end_matches(", ")); + let ty = format!( + "type D = {{ t: [{}], }}", + "T, ".repeat(41).trim_end_matches(", ") + ); assert_eq!(D::<&str, 41>::decl(), ty) } diff --git a/ts-rs/tests/optional_field.rs b/ts-rs/tests/optional_field.rs index 7f10496d1..1c7174a0b 100644 --- a/ts-rs/tests/optional_field.rs +++ b/ts-rs/tests/optional_field.rs @@ -24,11 +24,19 @@ fn in_struct() { fn in_enum() { #[derive(Serialize, TS)] enum Optional { - A { #[ts(optional)] a: Option }, - B { b: Option, } + A { + #[ts(optional)] + a: Option, + }, + B { + b: Option, + }, } - assert_eq!(Optional::inline(), r#"{ "A": { a?: number, } } | { "B": { b: string | null, } }"#); + assert_eq!( + Optional::inline(), + r#"{ "A": { a?: number, } } | { "B": { b: string | null, } }"# + ); } #[test] diff --git a/ts-rs/tests/ranges.rs b/ts-rs/tests/ranges.rs index 10cdcd659..10fd8ed83 100644 --- a/ts-rs/tests/ranges.rs +++ b/ts-rs/tests/ranges.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::ops::{Range, RangeInclusive}; use ts_rs::{Dependency, TS}; @@ -22,10 +23,11 @@ fn range() { "type RangeTest = { a: { start: number, end: number, }, b: { start: string, end: string, }, c: { start: { start: number, end: number, }, end: { start: number, end: number, }, }, d: { start: number, end: number, }, e: { start: Inner, end: Inner, }, }" ); assert_eq!( - RangeTest::dependencies(), - vec![ - Dependency::from_ty::().unwrap(), - Dependency::from_ty::().unwrap() - ] + RangeTest::dependencies() + .into_iter() + .collect::>() + .into_iter() + .collect::>(), + vec![Dependency::from_ty::().unwrap(),] ); }