Skip to content

Commit

Permalink
Merge pull request #221 from Aleph-Alpha/typelist
Browse files Browse the repository at this point in the history
Introduce TypeList, always export Dependencies as well
  • Loading branch information
escritorio-gustavo authored Feb 1, 2024
2 parents 317eb9e + dcec4d8 commit 8eb28e5
Show file tree
Hide file tree
Showing 16 changed files with 328 additions and 96 deletions.
2 changes: 1 addition & 1 deletion e2e/workspace/crate1/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ use ts_rs::TS;

#[derive(TS)]
pub struct Crate1 {
pub x: i32
pub x: [[[i32; 128]; 128]; 128],
}
2 changes: 1 addition & 1 deletion e2e/workspace/crate2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ use ts_rs::TS;

#[derive(TS)]
pub struct Crate2 {
pub x: i32
pub x: [[[i32; 128]; 128]; 128],
}
84 changes: 84 additions & 0 deletions e2e/workspace/parent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,87 @@ 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,
}
4 changes: 2 additions & 2 deletions macros/src/attr/field.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use syn::{Attribute, Ident, Result};
use syn::spanned::Spanned;
use syn::{Attribute, Ident, Result};

use crate::utils::parse_attrs;

Expand Down Expand Up @@ -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;
}
Expand Down
29 changes: 8 additions & 21 deletions macros/src/deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)*
}])
}
}
5 changes: 4 additions & 1 deletion macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,15 @@ impl DerivedTS {
#inline
}
#inline_flattened
fn dependencies() -> Vec<ts_rs::Dependency>

#[allow(clippy::unused_unit)]
fn dependency_types() -> impl ts_rs::typelist::TypeList
where
Self: 'static,
{
#dependencies
}

fn transparent() -> bool {
false
}
Expand Down
8 changes: 6 additions & 2 deletions macros/src/types/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>(&type_as)?, dependencies, generics),
(None, Some(type_as)) => {
format_type(&syn::parse_str::<Type>(&type_as)?, dependencies, generics)
}
(None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics),
};

Expand Down Expand Up @@ -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>(&type_as)?, dependencies, generics),
(None, Some(type_as)) => {
format_type(&syn::parse_str::<Type>(&type_as)?, dependencies, generics)
}
(None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics),
};

Expand Down
2 changes: 1 addition & 1 deletion macros/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`
Expand Down
8 changes: 0 additions & 8 deletions ts-rs/src/chrono.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dependency> { vec![] }
fn transparent() -> bool { false }
}
)*};
Expand All @@ -33,9 +31,6 @@ impl<T: TimeZone + 'static> TS for DateTime<T> {
fn inline() -> String {
"string".to_owned()
}
fn dependencies() -> Vec<Dependency> {
vec![]
}
fn transparent() -> bool {
false
}
Expand All @@ -51,9 +46,6 @@ impl<T: TimeZone + 'static> TS for Date<T> {
fn inline() -> String {
"string".to_owned()
}
fn dependencies() -> Vec<Dependency> {
vec![]
}
fn transparent() -> bool {
false
}
Expand Down
76 changes: 73 additions & 3 deletions ts-rs/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ use std::{
fmt::Write,
path::{Component, Path, PathBuf},
};
use std::sync::Mutex;

use thiserror::Error;

use ExportError::*;

use crate::TS;
Expand All @@ -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),
Expand All @@ -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<TypeId>,
error: Option<ExportError>,
}

impl<'a> TypeVisitor for Visit<'a> {
fn visit<T: TS + 'static + ?Sized>(&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::<T>(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<T: TS + ?Sized + 'static>(
) -> Result<(), ExportError> {
let mut seen = HashSet::new();
export_recursive::<T>(&mut seen)
}

// exports T, then recursively calls itself with all of its dependencies
fn export_recursive<T: TS + ?Sized + 'static>(
seen: &mut HashSet<TypeId>,
) -> Result<(), ExportError> {
if !seen.insert(TypeId::of::<T>()) {
return Ok(());
}

export_type::<T>()?;

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<T: TS + ?Sized + 'static>() -> Result<(), ExportError> {
let path = output_path::<T>()?;
Expand All @@ -36,6 +99,11 @@ pub(crate) fn export_type<T: TS + ?Sized + 'static>() -> Result<(), ExportError>
pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
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::<T>()?;

Expand All @@ -55,7 +123,9 @@ pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
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(())
}

Expand All @@ -72,7 +142,7 @@ pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String
fn output_path<T: TS + ?Sized>() -> Result<PathBuf, ExportError> {
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::<T>()))?);
Ok(manifest_dir.join(path))
}

Expand All @@ -84,7 +154,7 @@ fn generate_decl<T: TS + ?Sized>(out: &mut String) {

/// Push an import statement for all dependencies of `T`
fn generate_imports<T: TS + ?Sized + 'static>(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::<T>()))?);

let deps = T::dependencies();
let deduplicated_deps = deps
Expand Down
Loading

0 comments on commit 8eb28e5

Please sign in to comment.