From dfaa802c3649c5d261337517b0494fbc0d7dacf4 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 10:54:15 -0300 Subject: [PATCH 01/28] Allow for multiple types to be exported to the same file --- Cargo.lock | 1 + ts-rs/Cargo.toml | 1 + ts-rs/src/export.rs | 192 ++++++++++++--------------- ts-rs/src/export/path.rs | 61 ++++++++- ts-rs/src/lib.rs | 12 +- ts-rs/tests/integration/issue_308.rs | 10 +- ts-rs/tests/integration/main.rs | 1 + 7 files changed, 156 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8a0eae6..77bc6320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1191,6 +1191,7 @@ dependencies = [ "dprint-plugin-typescript", "heapless", "indexmap", + "lazy_static", "ordered-float", "semver", "serde", diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index 27a66611..3acd2b34 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -57,4 +57,5 @@ thiserror = "1" indexmap = { version = "2", optional = true } ordered-float = { version = ">= 3, < 5", optional = true } serde_json = { version = "1", optional = true } +lazy_static = { version = "1", default-features = false } diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 07d35dfc..513e3cf0 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -1,43 +1,37 @@ use std::{ any::TypeId, borrow::Cow, - collections::BTreeMap, + collections::{BTreeMap, HashMap, HashSet}, fmt::Write, fs::File, + io::{Seek, SeekFrom}, path::{Component, Path, PathBuf}, sync::Mutex, }; +pub use error::Error; +use lazy_static::lazy_static; +use path::diff_paths; pub(crate) use recursive_export::export_all_into; -use thiserror::Error; use crate::TS; +mod error; mod path; -const NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n"; - -/// An error which may occur when exporting a type -#[derive(Error, Debug)] -pub enum ExportError { - #[error("this type cannot be exported")] - CannotBeExported(&'static str), - #[cfg(feature = "format")] - #[error("an error occurred while formatting the generated typescript output")] - Formatting(String), - #[error("an error occurred while performing IO")] - Io(#[from] std::io::Error), - #[error("the environment variable CARGO_MANIFEST_DIR is not set")] - ManifestDirNotSet, +lazy_static! { + static ref EXPORT_PATHS: Mutex>> = Mutex::new(HashMap::new()); } +const NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n"; + mod recursive_export { use std::{any::TypeId, collections::HashSet, path::Path}; use super::export_into; use crate::{ typelist::{TypeList, TypeVisitor}, - ExportError, TS, + Error, TS, }; /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute within the given @@ -45,7 +39,7 @@ mod recursive_export { /// Additionally, all dependencies of `T` will be exported as well. pub(crate) fn export_all_into( out_dir: impl AsRef, - ) -> Result<(), ExportError> { + ) -> Result<(), Error> { let mut seen = HashSet::new(); export_recursive::(&mut seen, out_dir) } @@ -53,7 +47,7 @@ mod recursive_export { struct Visit<'a> { seen: &'a mut HashSet, out_dir: &'a Path, - error: Option, + error: Option, } impl<'a> TypeVisitor for Visit<'a> { @@ -72,7 +66,7 @@ mod recursive_export { fn export_recursive( seen: &mut HashSet, out_dir: impl AsRef, - ) -> Result<(), ExportError> { + ) -> Result<(), Error> { if !seen.insert(TypeId::of::()) { return Ok(()); } @@ -98,23 +92,19 @@ mod recursive_export { /// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute pub(crate) fn export_into( out_dir: impl AsRef, -) -> Result<(), ExportError> { +) -> Result<(), Error> { let path = T::output_path() .ok_or_else(std::any::type_name::) - .map_err(ExportError::CannotBeExported)?; + .map_err(Error::CannotBeExported)?; let path = out_dir.as_ref().join(path); export_to::(path::absolute(path)?) } /// Export `T` to the file specified by the `path` argument. -pub(crate) fn export_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(()); +pub(crate) fn export_to>(path: P) -> Result<(), Error> { + let path = path.as_ref().to_owned(); + let type_name = std::any::type_name::(); #[allow(unused_mut)] let mut buffer = export_to_string::()?; @@ -126,31 +116,75 @@ pub(crate) fn export_to>( let fmt_cfg = ConfigurationBuilder::new().deno().build(); if let Some(formatted) = format_text(path.as_ref(), &buffer, &fmt_cfg) - .map_err(|e| ExportError::Formatting(e.to_string()))? + .map_err(|e| Error::Formatting(e.to_string()))? { buffer = formatted; } } - if let Some(parent) = path.as_ref().parent() { + if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } - let lock = FILE_LOCK.lock().unwrap(); + { - // Manually write to file & call `sync_data`. Otherwise, calling `fs::read(path)` - // immediately after `T::export()` might result in an empty file. - use std::io::Write; - let mut file = File::create(path)?; - file.write_all(buffer.as_bytes())?; - file.sync_data()?; + use std::io::{Read, Write}; + + let mut lock = EXPORT_PATHS.lock().unwrap(); + + if let Some(entry) = lock.get_mut(&path) { + if !entry.contains(type_name) { + let (header, decl) = buffer.split_once("\n\n").unwrap(); + let imports = if header.len() > NOTE.len() { + &header[NOTE.len()..] + } else { + "" + }; + + let mut file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&path)?; + + let mut buf = [0; NOTE.len()]; + file.read_exact(&mut buf)?; + + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + + let imports = imports + .lines() + .filter(|x| !buf.contains(x)) + .collect::(); + + file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; + + file.write_all(imports.as_bytes())?; + if !imports.is_empty() { + file.write_all(b"\n")?; + } + file.write_all(buf.as_bytes())?; + + file.write_all(b"\n\n")?; + file.write_all(decl.as_bytes())?; + + entry.insert(type_name.to_owned()); + } + } else { + let mut file = File::create(&path)?; + file.write_all(buffer.as_bytes())?; + file.sync_data()?; + + let mut set = HashSet::new(); + set.insert(type_name.to_owned()); + lock.insert(path, set); + } } - drop(lock); Ok(()) } /// Returns the generated definition for `T`. -pub(crate) fn export_to_string() -> Result { +pub(crate) fn export_to_string() -> Result { let mut buffer = String::with_capacity(1024); buffer.push_str(NOTE); generate_imports::(&mut buffer, default_out_dir())?; @@ -182,11 +216,11 @@ fn generate_decl(out: &mut String) { fn generate_imports( out: &mut String, out_dir: impl AsRef, -) -> Result<(), ExportError> { +) -> Result<(), Error> { let path = T::output_path() .ok_or_else(std::any::type_name::) - .map_err(ExportError::CannotBeExported)?; - let path = out_dir.as_ref().join(path); + .map(|x| out_dir.as_ref().join(x)) + .map_err(Error::CannotBeExported)?; let deps = T::dependencies(); let deduplicated_deps = deps @@ -197,22 +231,20 @@ fn generate_imports( for (_, dep) in deduplicated_deps { let dep_path = out_dir.as_ref().join(dep.output_path); - let rel_path = import_path(&path, &dep_path); + let rel_path = import_path(&path, &dep_path)?; writeln!( out, "import type {{ {} }} from {:?};", &dep.ts_name, rel_path - ) - .unwrap(); + )?; } - writeln!(out).unwrap(); + writeln!(out)?; Ok(()) } /// Returns the required import path for importing `import` from the file `from` -fn import_path(from: &Path, import: &Path) -> String { - let rel_path = - diff_paths(import, from.parent().unwrap()).expect("failed to calculate import path"); +fn import_path(from: &Path, import: &Path) -> Result { + let rel_path = diff_paths(import, from.parent().unwrap())?; let path = match rel_path.components().next() { Some(Component::Normal(_)) => format!("./{}", rel_path.to_string_lossy()), _ => rel_path.to_string_lossy().into(), @@ -220,65 +252,9 @@ fn import_path(from: &Path, import: &Path) -> String { let path_without_extension = path.trim_end_matches(".ts"); - if cfg!(feature = "import-esm") { + Ok(if cfg!(feature = "import-esm") { format!("{}.js", path_without_extension) } else { path_without_extension.to_owned() - } -} - -// Construct a relative path from a provided base directory path to the provided path. -// -// Copyright 2012-2015 The Rust Project Developers. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. -// -// Adapted from rustc's path_relative_from -// https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158 -fn diff_paths(path: P, base: B) -> Result -where - P: AsRef, - B: AsRef, -{ - use Component as C; - - let path = path::absolute(path)?; - let base = path::absolute(base)?; - - let mut ita = path.components(); - let mut itb = base.components(); - let mut comps: Vec = vec![]; - - loop { - match (ita.next(), itb.next()) { - (Some(C::ParentDir | C::CurDir), _) | (_, Some(C::ParentDir | C::CurDir)) => { - unreachable!( - "The paths have been cleaned, no no '.' or '..' components are present" - ) - } - (None, None) => break, - (Some(a), None) => { - comps.push(a); - comps.extend(ita.by_ref()); - break; - } - (None, _) => comps.push(Component::ParentDir), - (Some(a), Some(b)) if comps.is_empty() && a == b => (), - (Some(a), Some(_)) => { - comps.push(Component::ParentDir); - for _ in itb { - comps.push(Component::ParentDir); - } - comps.push(a); - comps.extend(ita.by_ref()); - break; - } - } - } - - Ok(comps.iter().map(|c| c.as_os_str()).collect()) + }) } diff --git a/ts-rs/src/export/path.rs b/ts-rs/src/export/path.rs index be704834..7bcd3eb2 100644 --- a/ts-rs/src/export/path.rs +++ b/ts-rs/src/export/path.rs @@ -1,9 +1,10 @@ use std::path::{Component as C, Path, PathBuf}; -use super::ExportError as E; +use super::Error; const ERROR_MESSAGE: &str = r#"The path provided with `#[ts(export_to = "..")]` is not valid"#; -pub fn absolute>(path: T) -> Result { + +pub fn absolute>(path: T) -> Result { let path = path.as_ref(); if path.is_absolute() { @@ -17,7 +18,7 @@ pub fn absolute>(path: T) -> Result { match comp { C::CurDir => (), C::ParentDir => { - out.pop().ok_or(E::CannotBeExported(ERROR_MESSAGE))?; + out.pop().ok_or(Error::CannotBeExported(ERROR_MESSAGE))?; } comp => out.push(comp), } @@ -29,3 +30,57 @@ pub fn absolute>(path: T) -> Result { PathBuf::from(".") }) } + +// Construct a relative path from a provided base directory path to the provided path. +// +// Copyright 2012-2015 The Rust Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// Adapted from rustc's path_relative_from +// https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158 +pub(super) fn diff_paths(path: P, base: B) -> Result +where + P: AsRef, + B: AsRef, +{ + let path = absolute(path)?; + let base = absolute(base)?; + + let mut ita = path.components(); + let mut itb = base.components(); + let mut comps: Vec = vec![]; + + loop { + match (ita.next(), itb.next()) { + (Some(C::ParentDir | C::CurDir), _) | (_, Some(C::ParentDir | C::CurDir)) => { + unreachable!( + "The paths have been cleaned, no no '.' or '..' components are present" + ) + } + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(C::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(_)) => { + comps.push(C::ParentDir); + for _ in itb { + comps.push(C::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + + Ok(comps.iter().map(|c| c.as_os_str()).collect()) +} diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 5eb7d0e3..3e14cd46 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -130,7 +130,7 @@ use std::{ pub use ts_rs_macros::TS; -pub use crate::export::ExportError; +pub use crate::export::Error; use crate::typelist::TypeList; #[cfg(feature = "chrono-impl")] @@ -475,13 +475,13 @@ pub trait TS { /// /// To alter the filename or path of the type within the target directory, /// use `#[ts(export_to = "...")]`. - fn export() -> Result<(), ExportError> + fn export() -> Result<(), Error> where Self: 'static, { let path = Self::default_output_path() .ok_or_else(std::any::type_name::) - .map_err(ExportError::CannotBeExported)?; + .map_err(Error::CannotBeExported)?; export::export_to::(path) } @@ -502,7 +502,7 @@ pub trait TS { /// /// To alter the filenames or paths of the types within the target directory, /// use `#[ts(export_to = "...")]`. - fn export_all() -> Result<(), ExportError> + fn export_all() -> Result<(), Error> where Self: 'static, { @@ -522,7 +522,7 @@ pub trait TS { /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be /// exported automatically whenever `cargo test` is run. /// In that case, there is no need to manually call this function. - fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> + fn export_all_to(out_dir: impl AsRef) -> Result<(), Error> where Self: 'static, { @@ -536,7 +536,7 @@ pub trait TS { /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be /// exported automatically whenever `cargo test` is run. /// In that case, there is no need to manually call this function. - fn export_to_string() -> Result + fn export_to_string() -> Result where Self: 'static, { diff --git a/ts-rs/tests/integration/issue_308.rs b/ts-rs/tests/integration/issue_308.rs index 6e2faf5b..6d3a04c9 100644 --- a/ts-rs/tests/integration/issue_308.rs +++ b/ts-rs/tests/integration/issue_308.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use ts_rs::{typelist::TypeList, Dependency, ExportError, TS}; +use ts_rs::{typelist::TypeList, Dependency, Error, TS}; #[rustfmt::skip] trait Malicious { @@ -16,10 +16,10 @@ trait Malicious { fn dependency_types() -> impl TypeList {} fn generics() -> impl TypeList {} fn dependencies() -> Vec { unimplemented!() } - fn export() -> Result<(), ExportError> { unimplemented!() } - fn export_all() -> Result<(), ExportError> { unimplemented!() } - fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> { unimplemented!() } - fn export_to_string() -> Result { unimplemented!() } + fn export() -> Result<(), Error> { unimplemented!() } + fn export_all() -> Result<(), Error> { unimplemented!() } + fn export_all_to(out_dir: impl AsRef) -> Result<(), Error> { unimplemented!() } + fn export_to_string() -> Result { unimplemented!() } fn output_path() -> Option<&'static Path> { unimplemented!() } fn default_output_path() -> Option { unimplemented!() } } diff --git a/ts-rs/tests/integration/main.rs b/ts-rs/tests/integration/main.rs index 62bbeab0..971d02fb 100644 --- a/ts-rs/tests/integration/main.rs +++ b/ts-rs/tests/integration/main.rs @@ -35,6 +35,7 @@ mod ranges; mod raw_idents; mod recursion_limit; mod references; +mod same_file_export; mod self_referential; mod semver; mod serde_json; From 784eecd788ecda92361f03b679772e6f2c12d886 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 10:54:15 -0300 Subject: [PATCH 02/28] Allow for multiple types to be exported to the same file --- Cargo.lock | 1 + ts-rs/Cargo.toml | 1 + ts-rs/src/export.rs | 192 +++++++++----------- ts-rs/src/export/error.rs | 15 ++ ts-rs/src/export/path.rs | 61 ++++++- ts-rs/src/lib.rs | 12 +- ts-rs/tests/integration/issue_308.rs | 10 +- ts-rs/tests/integration/main.rs | 1 + ts-rs/tests/integration/same_file_export.rs | 32 ++++ 9 files changed, 203 insertions(+), 122 deletions(-) create mode 100644 ts-rs/src/export/error.rs create mode 100644 ts-rs/tests/integration/same_file_export.rs diff --git a/Cargo.lock b/Cargo.lock index c8a0eae6..77bc6320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1191,6 +1191,7 @@ dependencies = [ "dprint-plugin-typescript", "heapless", "indexmap", + "lazy_static", "ordered-float", "semver", "serde", diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index 27a66611..3acd2b34 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -57,4 +57,5 @@ thiserror = "1" indexmap = { version = "2", optional = true } ordered-float = { version = ">= 3, < 5", optional = true } serde_json = { version = "1", optional = true } +lazy_static = { version = "1", default-features = false } diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 07d35dfc..513e3cf0 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -1,43 +1,37 @@ use std::{ any::TypeId, borrow::Cow, - collections::BTreeMap, + collections::{BTreeMap, HashMap, HashSet}, fmt::Write, fs::File, + io::{Seek, SeekFrom}, path::{Component, Path, PathBuf}, sync::Mutex, }; +pub use error::Error; +use lazy_static::lazy_static; +use path::diff_paths; pub(crate) use recursive_export::export_all_into; -use thiserror::Error; use crate::TS; +mod error; mod path; -const NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n"; - -/// An error which may occur when exporting a type -#[derive(Error, Debug)] -pub enum ExportError { - #[error("this type cannot be exported")] - CannotBeExported(&'static str), - #[cfg(feature = "format")] - #[error("an error occurred while formatting the generated typescript output")] - Formatting(String), - #[error("an error occurred while performing IO")] - Io(#[from] std::io::Error), - #[error("the environment variable CARGO_MANIFEST_DIR is not set")] - ManifestDirNotSet, +lazy_static! { + static ref EXPORT_PATHS: Mutex>> = Mutex::new(HashMap::new()); } +const NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n"; + mod recursive_export { use std::{any::TypeId, collections::HashSet, path::Path}; use super::export_into; use crate::{ typelist::{TypeList, TypeVisitor}, - ExportError, TS, + Error, TS, }; /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute within the given @@ -45,7 +39,7 @@ mod recursive_export { /// Additionally, all dependencies of `T` will be exported as well. pub(crate) fn export_all_into( out_dir: impl AsRef, - ) -> Result<(), ExportError> { + ) -> Result<(), Error> { let mut seen = HashSet::new(); export_recursive::(&mut seen, out_dir) } @@ -53,7 +47,7 @@ mod recursive_export { struct Visit<'a> { seen: &'a mut HashSet, out_dir: &'a Path, - error: Option, + error: Option, } impl<'a> TypeVisitor for Visit<'a> { @@ -72,7 +66,7 @@ mod recursive_export { fn export_recursive( seen: &mut HashSet, out_dir: impl AsRef, - ) -> Result<(), ExportError> { + ) -> Result<(), Error> { if !seen.insert(TypeId::of::()) { return Ok(()); } @@ -98,23 +92,19 @@ mod recursive_export { /// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute pub(crate) fn export_into( out_dir: impl AsRef, -) -> Result<(), ExportError> { +) -> Result<(), Error> { let path = T::output_path() .ok_or_else(std::any::type_name::) - .map_err(ExportError::CannotBeExported)?; + .map_err(Error::CannotBeExported)?; let path = out_dir.as_ref().join(path); export_to::(path::absolute(path)?) } /// Export `T` to the file specified by the `path` argument. -pub(crate) fn export_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(()); +pub(crate) fn export_to>(path: P) -> Result<(), Error> { + let path = path.as_ref().to_owned(); + let type_name = std::any::type_name::(); #[allow(unused_mut)] let mut buffer = export_to_string::()?; @@ -126,31 +116,75 @@ pub(crate) fn export_to>( let fmt_cfg = ConfigurationBuilder::new().deno().build(); if let Some(formatted) = format_text(path.as_ref(), &buffer, &fmt_cfg) - .map_err(|e| ExportError::Formatting(e.to_string()))? + .map_err(|e| Error::Formatting(e.to_string()))? { buffer = formatted; } } - if let Some(parent) = path.as_ref().parent() { + if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } - let lock = FILE_LOCK.lock().unwrap(); + { - // Manually write to file & call `sync_data`. Otherwise, calling `fs::read(path)` - // immediately after `T::export()` might result in an empty file. - use std::io::Write; - let mut file = File::create(path)?; - file.write_all(buffer.as_bytes())?; - file.sync_data()?; + use std::io::{Read, Write}; + + let mut lock = EXPORT_PATHS.lock().unwrap(); + + if let Some(entry) = lock.get_mut(&path) { + if !entry.contains(type_name) { + let (header, decl) = buffer.split_once("\n\n").unwrap(); + let imports = if header.len() > NOTE.len() { + &header[NOTE.len()..] + } else { + "" + }; + + let mut file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&path)?; + + let mut buf = [0; NOTE.len()]; + file.read_exact(&mut buf)?; + + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + + let imports = imports + .lines() + .filter(|x| !buf.contains(x)) + .collect::(); + + file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; + + file.write_all(imports.as_bytes())?; + if !imports.is_empty() { + file.write_all(b"\n")?; + } + file.write_all(buf.as_bytes())?; + + file.write_all(b"\n\n")?; + file.write_all(decl.as_bytes())?; + + entry.insert(type_name.to_owned()); + } + } else { + let mut file = File::create(&path)?; + file.write_all(buffer.as_bytes())?; + file.sync_data()?; + + let mut set = HashSet::new(); + set.insert(type_name.to_owned()); + lock.insert(path, set); + } } - drop(lock); Ok(()) } /// Returns the generated definition for `T`. -pub(crate) fn export_to_string() -> Result { +pub(crate) fn export_to_string() -> Result { let mut buffer = String::with_capacity(1024); buffer.push_str(NOTE); generate_imports::(&mut buffer, default_out_dir())?; @@ -182,11 +216,11 @@ fn generate_decl(out: &mut String) { fn generate_imports( out: &mut String, out_dir: impl AsRef, -) -> Result<(), ExportError> { +) -> Result<(), Error> { let path = T::output_path() .ok_or_else(std::any::type_name::) - .map_err(ExportError::CannotBeExported)?; - let path = out_dir.as_ref().join(path); + .map(|x| out_dir.as_ref().join(x)) + .map_err(Error::CannotBeExported)?; let deps = T::dependencies(); let deduplicated_deps = deps @@ -197,22 +231,20 @@ fn generate_imports( for (_, dep) in deduplicated_deps { let dep_path = out_dir.as_ref().join(dep.output_path); - let rel_path = import_path(&path, &dep_path); + let rel_path = import_path(&path, &dep_path)?; writeln!( out, "import type {{ {} }} from {:?};", &dep.ts_name, rel_path - ) - .unwrap(); + )?; } - writeln!(out).unwrap(); + writeln!(out)?; Ok(()) } /// Returns the required import path for importing `import` from the file `from` -fn import_path(from: &Path, import: &Path) -> String { - let rel_path = - diff_paths(import, from.parent().unwrap()).expect("failed to calculate import path"); +fn import_path(from: &Path, import: &Path) -> Result { + let rel_path = diff_paths(import, from.parent().unwrap())?; let path = match rel_path.components().next() { Some(Component::Normal(_)) => format!("./{}", rel_path.to_string_lossy()), _ => rel_path.to_string_lossy().into(), @@ -220,65 +252,9 @@ fn import_path(from: &Path, import: &Path) -> String { let path_without_extension = path.trim_end_matches(".ts"); - if cfg!(feature = "import-esm") { + Ok(if cfg!(feature = "import-esm") { format!("{}.js", path_without_extension) } else { path_without_extension.to_owned() - } -} - -// Construct a relative path from a provided base directory path to the provided path. -// -// Copyright 2012-2015 The Rust Project Developers. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. -// -// Adapted from rustc's path_relative_from -// https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158 -fn diff_paths(path: P, base: B) -> Result -where - P: AsRef, - B: AsRef, -{ - use Component as C; - - let path = path::absolute(path)?; - let base = path::absolute(base)?; - - let mut ita = path.components(); - let mut itb = base.components(); - let mut comps: Vec = vec![]; - - loop { - match (ita.next(), itb.next()) { - (Some(C::ParentDir | C::CurDir), _) | (_, Some(C::ParentDir | C::CurDir)) => { - unreachable!( - "The paths have been cleaned, no no '.' or '..' components are present" - ) - } - (None, None) => break, - (Some(a), None) => { - comps.push(a); - comps.extend(ita.by_ref()); - break; - } - (None, _) => comps.push(Component::ParentDir), - (Some(a), Some(b)) if comps.is_empty() && a == b => (), - (Some(a), Some(_)) => { - comps.push(Component::ParentDir); - for _ in itb { - comps.push(Component::ParentDir); - } - comps.push(a); - comps.extend(ita.by_ref()); - break; - } - } - } - - Ok(comps.iter().map(|c| c.as_os_str()).collect()) + }) } diff --git a/ts-rs/src/export/error.rs b/ts-rs/src/export/error.rs new file mode 100644 index 00000000..e3add3c4 --- /dev/null +++ b/ts-rs/src/export/error.rs @@ -0,0 +1,15 @@ +/// An error which may occur when exporting a type +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("this type cannot be exported")] + CannotBeExported(&'static str), + #[cfg(feature = "format")] + #[error("an error occurred while formatting the generated typescript output")] + Formatting(String), + #[error("an error occurred while performing IO")] + Io(#[from] std::io::Error), + #[error("the environment variable CARGO_MANIFEST_DIR is not set")] + ManifestDirNotSet, + #[error("an error occurred while writing to a formatted buffer")] + Fmt(#[from] std::fmt::Error), +} diff --git a/ts-rs/src/export/path.rs b/ts-rs/src/export/path.rs index be704834..7bcd3eb2 100644 --- a/ts-rs/src/export/path.rs +++ b/ts-rs/src/export/path.rs @@ -1,9 +1,10 @@ use std::path::{Component as C, Path, PathBuf}; -use super::ExportError as E; +use super::Error; const ERROR_MESSAGE: &str = r#"The path provided with `#[ts(export_to = "..")]` is not valid"#; -pub fn absolute>(path: T) -> Result { + +pub fn absolute>(path: T) -> Result { let path = path.as_ref(); if path.is_absolute() { @@ -17,7 +18,7 @@ pub fn absolute>(path: T) -> Result { match comp { C::CurDir => (), C::ParentDir => { - out.pop().ok_or(E::CannotBeExported(ERROR_MESSAGE))?; + out.pop().ok_or(Error::CannotBeExported(ERROR_MESSAGE))?; } comp => out.push(comp), } @@ -29,3 +30,57 @@ pub fn absolute>(path: T) -> Result { PathBuf::from(".") }) } + +// Construct a relative path from a provided base directory path to the provided path. +// +// Copyright 2012-2015 The Rust Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// Adapted from rustc's path_relative_from +// https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158 +pub(super) fn diff_paths(path: P, base: B) -> Result +where + P: AsRef, + B: AsRef, +{ + let path = absolute(path)?; + let base = absolute(base)?; + + let mut ita = path.components(); + let mut itb = base.components(); + let mut comps: Vec = vec![]; + + loop { + match (ita.next(), itb.next()) { + (Some(C::ParentDir | C::CurDir), _) | (_, Some(C::ParentDir | C::CurDir)) => { + unreachable!( + "The paths have been cleaned, no no '.' or '..' components are present" + ) + } + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(C::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(_)) => { + comps.push(C::ParentDir); + for _ in itb { + comps.push(C::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + + Ok(comps.iter().map(|c| c.as_os_str()).collect()) +} diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 5eb7d0e3..3e14cd46 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -130,7 +130,7 @@ use std::{ pub use ts_rs_macros::TS; -pub use crate::export::ExportError; +pub use crate::export::Error; use crate::typelist::TypeList; #[cfg(feature = "chrono-impl")] @@ -475,13 +475,13 @@ pub trait TS { /// /// To alter the filename or path of the type within the target directory, /// use `#[ts(export_to = "...")]`. - fn export() -> Result<(), ExportError> + fn export() -> Result<(), Error> where Self: 'static, { let path = Self::default_output_path() .ok_or_else(std::any::type_name::) - .map_err(ExportError::CannotBeExported)?; + .map_err(Error::CannotBeExported)?; export::export_to::(path) } @@ -502,7 +502,7 @@ pub trait TS { /// /// To alter the filenames or paths of the types within the target directory, /// use `#[ts(export_to = "...")]`. - fn export_all() -> Result<(), ExportError> + fn export_all() -> Result<(), Error> where Self: 'static, { @@ -522,7 +522,7 @@ pub trait TS { /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be /// exported automatically whenever `cargo test` is run. /// In that case, there is no need to manually call this function. - fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> + fn export_all_to(out_dir: impl AsRef) -> Result<(), Error> where Self: 'static, { @@ -536,7 +536,7 @@ pub trait TS { /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be /// exported automatically whenever `cargo test` is run. /// In that case, there is no need to manually call this function. - fn export_to_string() -> Result + fn export_to_string() -> Result where Self: 'static, { diff --git a/ts-rs/tests/integration/issue_308.rs b/ts-rs/tests/integration/issue_308.rs index 6e2faf5b..6d3a04c9 100644 --- a/ts-rs/tests/integration/issue_308.rs +++ b/ts-rs/tests/integration/issue_308.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use ts_rs::{typelist::TypeList, Dependency, ExportError, TS}; +use ts_rs::{typelist::TypeList, Dependency, Error, TS}; #[rustfmt::skip] trait Malicious { @@ -16,10 +16,10 @@ trait Malicious { fn dependency_types() -> impl TypeList {} fn generics() -> impl TypeList {} fn dependencies() -> Vec { unimplemented!() } - fn export() -> Result<(), ExportError> { unimplemented!() } - fn export_all() -> Result<(), ExportError> { unimplemented!() } - fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> { unimplemented!() } - fn export_to_string() -> Result { unimplemented!() } + fn export() -> Result<(), Error> { unimplemented!() } + fn export_all() -> Result<(), Error> { unimplemented!() } + fn export_all_to(out_dir: impl AsRef) -> Result<(), Error> { unimplemented!() } + fn export_to_string() -> Result { unimplemented!() } fn output_path() -> Option<&'static Path> { unimplemented!() } fn default_output_path() -> Option { unimplemented!() } } diff --git a/ts-rs/tests/integration/main.rs b/ts-rs/tests/integration/main.rs index 62bbeab0..971d02fb 100644 --- a/ts-rs/tests/integration/main.rs +++ b/ts-rs/tests/integration/main.rs @@ -35,6 +35,7 @@ mod ranges; mod raw_idents; mod recursion_limit; mod references; +mod same_file_export; mod self_referential; mod semver; mod serde_json; diff --git a/ts-rs/tests/integration/same_file_export.rs b/ts-rs/tests/integration/same_file_export.rs new file mode 100644 index 00000000..389bb6d7 --- /dev/null +++ b/ts-rs/tests/integration/same_file_export.rs @@ -0,0 +1,32 @@ +use ts_rs::TS; + +#[derive(TS)] +#[ts(export, export_to = "same_file_export/")] +struct DepA { + foo: i32, +} + +#[derive(TS)] +#[ts(export, export_to = "same_file_export/")] +struct DepB { + foo: i32, +} + +#[derive(TS)] +#[ts(export, export_to = "same_file_export/types.ts")] +struct A { + foo: DepA, +} + +#[derive(TS)] +#[ts(export, export_to = "same_file_export/types.ts")] +struct B { + foo: DepB, +} + +#[derive(TS)] +#[ts(export, export_to = "same_file_export/types.ts")] +struct C { + foo: DepA, + bar: DepB, +} From 294126cbe7ff65161d1c3a1f69168c269849e464 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 11:08:24 -0300 Subject: [PATCH 03/28] Prevent duplication --- ts-rs/src/export.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 513e3cf0..f6e78b3c 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -104,7 +104,7 @@ pub(crate) fn export_into( /// Export `T` to the file specified by the `path` argument. pub(crate) fn export_to>(path: P) -> Result<(), Error> { let path = path.as_ref().to_owned(); - let type_name = std::any::type_name::(); + let type_name = T::ident(); #[allow(unused_mut)] let mut buffer = export_to_string::()?; @@ -132,7 +132,7 @@ pub(crate) fn export_to>(path: P) -> Re let mut lock = EXPORT_PATHS.lock().unwrap(); if let Some(entry) = lock.get_mut(&path) { - if !entry.contains(type_name) { + if !entry.contains(&type_name) { let (header, decl) = buffer.split_once("\n\n").unwrap(); let imports = if header.len() > NOTE.len() { &header[NOTE.len()..] From 7312fa1008d6c9cf0c02160545796153f9982eb3 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 14:19:45 -0300 Subject: [PATCH 04/28] If types exported to the same file depend on each other, they should not generate an import statement --- ts-rs/src/export.rs | 14 ++++++++++++++ ts-rs/tests/integration/same_file_export.rs | 1 + 2 files changed, 15 insertions(+) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index f6e78b3c..07e95a18 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -232,6 +232,20 @@ fn generate_imports( for (_, dep) in deduplicated_deps { let dep_path = out_dir.as_ref().join(dep.output_path); let rel_path = import_path(&path, &dep_path)?; + + let is_same_file = path + .file_name() + .and_then(std::ffi::OsStr::to_str) + .map(|x| x.trim_end_matches(".js")) + .map(|x| x.trim_end_matches(".ts")) + .map(|x| format!("./{x}")) + .map(|x| x == rel_path) + .unwrap_or(false); + + if is_same_file { + continue; + } + writeln!( out, "import type {{ {} }} from {:?};", diff --git a/ts-rs/tests/integration/same_file_export.rs b/ts-rs/tests/integration/same_file_export.rs index 389bb6d7..3a9eaeec 100644 --- a/ts-rs/tests/integration/same_file_export.rs +++ b/ts-rs/tests/integration/same_file_export.rs @@ -29,4 +29,5 @@ struct B { struct C { foo: DepA, bar: DepB, + biz: B, } From 4dc1c173b306e24a7e0dd9b3e65d3c651241df3a Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 14:21:47 -0300 Subject: [PATCH 05/28] Fix import-esm case --- ts-rs/src/export.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 07e95a18..523ebd9b 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -236,7 +236,6 @@ fn generate_imports( let is_same_file = path .file_name() .and_then(std::ffi::OsStr::to_str) - .map(|x| x.trim_end_matches(".js")) .map(|x| x.trim_end_matches(".ts")) .map(|x| format!("./{x}")) .map(|x| x == rel_path) From 892b11b0f4e15c97c06ba413e87e0c0b862c9b44 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 14:25:06 -0300 Subject: [PATCH 06/28] Fix import-esm case --- ts-rs/src/export.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 523ebd9b..a6bcd18a 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -238,7 +238,7 @@ fn generate_imports( .and_then(std::ffi::OsStr::to_str) .map(|x| x.trim_end_matches(".ts")) .map(|x| format!("./{x}")) - .map(|x| x == rel_path) + .map(|x| x == rel_path.trim_end_matches(".js")) .unwrap_or(false); if is_same_file { From 27832ee9adf2e314ef668f0a81ce16cc65461938 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 14:33:02 -0300 Subject: [PATCH 07/28] Change read_exact for seek --- ts-rs/src/export.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index a6bcd18a..69dbd201 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -145,8 +145,7 @@ pub(crate) fn export_to>(path: P) -> Re .write(true) .open(&path)?; - let mut buf = [0; NOTE.len()]; - file.read_exact(&mut buf)?; + file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; let mut buf = String::new(); file.read_to_string(&mut buf)?; From c7b170eb39605334265c5bf9152aaa4847456a31 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 10 May 2024 15:01:15 -0300 Subject: [PATCH 08/28] Remove unneded clone --- ts-rs/src/export.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 69dbd201..145f9d44 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -166,7 +166,7 @@ pub(crate) fn export_to>(path: P) -> Re file.write_all(b"\n\n")?; file.write_all(decl.as_bytes())?; - entry.insert(type_name.to_owned()); + entry.insert(type_name); } } else { let mut file = File::create(&path)?; @@ -174,7 +174,7 @@ pub(crate) fn export_to>(path: P) -> Re file.sync_data()?; let mut set = HashSet::new(); - set.insert(type_name.to_owned()); + set.insert(type_name); lock.insert(path, set); } } From 8d32a0e95a18b45242966d9b0d7dec713a6755d4 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 20 May 2024 08:45:35 -0300 Subject: [PATCH 09/28] Merge branch 'main' into export_same_file --- .github/workflows/test.yml | 15 +- CHANGELOG.md | 4 + README.md | 2 +- macros/src/attr/enum.rs | 4 +- macros/src/attr/field.rs | 13 +- macros/src/attr/mod.rs | 2 - macros/src/attr/struct.rs | 4 +- macros/src/attr/variant.rs | 4 +- macros/src/deps.rs | 14 +- macros/src/lib.rs | 16 +- macros/src/utils.rs | 20 ++- ts-rs/Cargo.toml | 2 +- ts-rs/src/export.rs | 7 +- ts-rs/src/lib.rs | 140 ++++++++++-------- ts-rs/src/typelist.rs | 105 ------------- ts-rs/tests/integration/issue_308.rs | 6 +- ts-rs/tests/integration/issue_317.rs | 18 +++ ts-rs/tests/integration/main.rs | 1 + ts-rs/tests/integration/recursion_limit.rs | 36 ++++- .../integration/serde_skip_with_default.rs | 1 + ts-rs/tests/integration/struct_rename.rs | 4 - ts-rs/tests/integration/tuple.rs | 57 ++++++- 22 files changed, 245 insertions(+), 230 deletions(-) delete mode 100644 ts-rs/src/typelist.rs create mode 100644 ts-rs/tests/integration/issue_317.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bed59e4e..e27ed4b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,11 +100,12 @@ jobs: toolchain: stable - uses: Swatinem/rust-cache@v2 - name: Test + working-directory: ts-rs run: | cargo test --all-features shopt -s globstar - tsc ts-rs/bindings/**/*.ts --noEmit --noUnusedLocals --strict - rm -rf ts-rs/bindings + tsc bindings/**/*.ts --noEmit --noUnusedLocals --strict + rm -rf bindings test-export-env: name: Test ts-rs with TS_RS_EXPORT_DIR @@ -118,11 +119,12 @@ jobs: toolchain: stable - uses: Swatinem/rust-cache@v2 - name: Test + working-directory: ts-rs run: | TS_RS_EXPORT_DIR=output cargo test --no-default-features shopt -s globstar - tsc ts-rs/output/**/*.ts --noEmit --noUnusedLocals --strict - rm -rf ts-rs/output + tsc output/**/*.ts --noEmit --noUnusedLocals --strict + rm -rf output test-no-features: name: Test ts-rs with --no-default-features @@ -136,8 +138,9 @@ jobs: toolchain: stable - uses: Swatinem/rust-cache@v2 - name: Test + working-directory: ts-rs run: | cargo test --no-default-features shopt -s globstar - tsc ts-rs/bindings/**/*.ts --noEmit --noUnusedLocals - rm -rf ts-rs/bindings + tsc bindings/**/*.ts --noEmit --noUnusedLocals + rm -rf bindings diff --git a/CHANGELOG.md b/CHANGELOG.md index fb011913..1bc7f5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ - `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]` ([#280](https://github.com/Aleph-Alpha/ts-rs/pull/280)) - Fix incompatibility with serde for `snake_case`, `kebab-case` and `SCREAMING_SNAKE_CASE` ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298)) - `#[ts(rename_all = "...")]` no longer accepts variations in the string's casing, dashes and underscores to make behavior consistent with serde ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298)) +- Remove `TypeList`, and replace `TS::dependency_types`/`TS::generics` with `TS::visit_dependencies`/`TS::visit_generics`. + This finally resolves "overflow evaluating the requirement", "reached the recursion limit" errors. + Also, compile times should benefit. This is a technically breaking change for those interacting with the `TS` trait + directly. For those just using `#[derive(TS)]` and `#[ts(...)]`, nothing changes! ### Features diff --git a/README.md b/README.md index 623ca0b2..6d019aee 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,6 @@ Feel free to open an issue, discuss using GitHub discussions or open a PR. [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md) ### MSRV -The Minimum Supported Rust Version for this crate is 1.75.0 +The Minimum Supported Rust Version for this crate is 1.63.0 License: MIT diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index 5532a69b..21ff59c1 100644 --- a/macros/src/attr/enum.rs +++ b/macros/src/attr/enum.rs @@ -50,8 +50,7 @@ impl EnumAttr { pub fn from_attrs(attrs: &[Attribute]) -> Result { let mut result = parse_attrs::(attrs)?; - #[cfg(feature = "serde-compat")] - { + if cfg!(feature = "serde-compat") { let serde_attr = crate::utils::parse_serde_attrs::(attrs); result = result.merge(serde_attr.0); } @@ -223,7 +222,6 @@ impl_parse! { } } -#[cfg(feature = "serde-compat")] impl_parse! { Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index dd8552c0..dde475d4 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -18,7 +18,6 @@ pub struct FieldAttr { pub flatten: bool, pub docs: String, - #[cfg(feature = "serde-compat")] pub using_serde_with: bool, } @@ -35,8 +34,7 @@ impl FieldAttr { pub fn from_attrs(attrs: &[Attribute]) -> Result { let mut result = parse_attrs::(attrs)?; - #[cfg(feature = "serde-compat")] - if !result.skip { + if cfg!(feature = "serde-compat") && !result.skip { let serde_attr = crate::utils::parse_serde_attrs::(attrs); result = result.merge(serde_attr.0); } @@ -71,7 +69,7 @@ impl Attr for FieldAttr { nullable: self.optional.nullable || other.optional.nullable, }, flatten: self.flatten || other.flatten, - #[cfg(feature = "serde-compat")] + using_serde_with: self.using_serde_with || other.using_serde_with, // We can't emit TSDoc for a flattened field @@ -86,8 +84,10 @@ impl Attr for FieldAttr { } fn assert_validity(&self, field: &Self::Item) -> Result<()> { - #[cfg(feature = "serde-compat")] - if self.using_serde_with && !(self.type_as.is_some() || self.type_override.is_some()) { + if cfg!(feature = "serde-compat") + && self.using_serde_with + && !(self.type_as.is_some() || self.type_override.is_some()) + { syn_err_spanned!( field; r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"# @@ -196,7 +196,6 @@ impl_parse! { } } -#[cfg(feature = "serde-compat")] impl_parse! { Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs index 196166f3..27d12eb9 100644 --- a/macros/src/attr/mod.rs +++ b/macros/src/attr/mod.rs @@ -38,13 +38,11 @@ pub(super) trait ContainerAttr: Attr { fn crate_rename(&self) -> Path; } -#[cfg(feature = "serde-compat")] #[derive(Default)] pub(super) struct Serde(pub T) where T: Attr; -#[cfg(feature = "serde-compat")] impl Serde where T: Attr, diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index 69a4b067..46138015 100644 --- a/macros/src/attr/struct.rs +++ b/macros/src/attr/struct.rs @@ -30,8 +30,7 @@ impl StructAttr { pub fn from_attrs(attrs: &[Attribute]) -> Result { let mut result = parse_attrs::(attrs)?; - #[cfg(feature = "serde-compat")] - { + if cfg!(feature = "serde-compat") { let serde_attr = crate::utils::parse_serde_attrs::(attrs); result = result.merge(serde_attr.0); } @@ -145,7 +144,6 @@ impl_parse! { } } -#[cfg(feature = "serde-compat")] impl_parse! { Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs index 52daedad..cd1fee9d 100644 --- a/macros/src/attr/variant.rs +++ b/macros/src/attr/variant.rs @@ -18,8 +18,7 @@ pub struct VariantAttr { impl VariantAttr { pub fn from_attrs(attrs: &[Attribute]) -> Result { let mut result = parse_attrs::(attrs)?; - #[cfg(feature = "serde-compat")] - if !result.skip { + if cfg!(feature = "serde-compat") && !result.skip { let serde_attr = crate::utils::parse_serde_attrs::(attrs); result = result.merge(serde_attr.0); } @@ -62,7 +61,6 @@ impl_parse! { } } -#[cfg(feature = "serde-compat")] impl_parse! { Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), diff --git a/macros/src/deps.rs b/macros/src/deps.rs index e38c96c5..ef61084a 100644 --- a/macros/src/deps.rs +++ b/macros/src/deps.rs @@ -79,13 +79,11 @@ impl Dependencies { impl ToTokens for Dependencies { fn to_tokens(&self, tokens: &mut TokenStream) { - let crate_rename = &self.crate_rename; let lines = self.dependencies.iter(); - tokens.extend(quote![{ - use #crate_rename::typelist::TypeList; - ()#(#lines)* - }]); + tokens.extend(quote![ + #(#lines;)* + ]); } } @@ -93,12 +91,12 @@ impl ToTokens for Dependency { fn to_tokens(&self, tokens: &mut TokenStream) { tokens.extend(match self { Dependency::Transitive { crate_rename, ty } => { - quote![.extend(<#ty as #crate_rename::TS>::dependency_types())] + quote![<#ty as #crate_rename::TS>::visit_dependencies(v)] } Dependency::Generics { crate_rename, ty } => { - quote![.extend(<#ty as #crate_rename::TS>::generics())] + quote![<#ty as #crate_rename::TS>::visit_generics(v)] } - Dependency::Type(ty) => quote![.push::<#ty>()], + Dependency::Type(ty) => quote![v.visit::<#ty>()], }); } } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 9e1f40aa..845d2af3 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -92,8 +92,7 @@ impl DerivedTS { #generics_fn #output_path_fn - #[allow(clippy::unused_unit)] - fn dependency_types() -> impl #crate_rename::typelist::TypeList + fn visit_dependencies(v: &mut impl #crate_rename::TypeVisitor) where Self: 'static, { @@ -195,15 +194,18 @@ impl DerivedTS { let generics = generics .type_params() .filter(|ty| !self.concrete.contains_key(&ty.ident)) - .map(|TypeParam { ident, .. }| quote![.push::<#ident>().extend(<#ident as #crate_rename::TS>::generics())]); + .map(|TypeParam { ident, .. }| { + quote![ + v.visit::<#ident>(); + <#ident as #crate_rename::TS>::visit_generics(v); + ] + }); quote! { - #[allow(clippy::unused_unit)] - fn generics() -> impl #crate_rename::typelist::TypeList + fn visit_generics(v: &mut impl #crate_rename::TypeVisitor) where Self: 'static, { - use #crate_rename::typelist::TypeList; - ()#(#generics)* + #(#generics)* } } } diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 51511d3d..07ce47a2 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -97,7 +97,7 @@ pub fn raw_name_to_ts_field(value: String) -> String { } /// Parse all `#[ts(..)]` attributes from the given slice. -pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result +pub(crate) fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result where A: TryFrom<&'a Attribute, Error = Error> + Attr, { @@ -111,15 +111,11 @@ where } /// Parse all `#[serde(..)]` attributes from the given slice. -#[cfg(feature = "serde-compat")] -#[allow(unused)] pub fn parse_serde_attrs<'a, A>(attrs: &'a [Attribute]) -> Serde where A: Attr, Serde: TryFrom<&'a Attribute, Error = Error>, { - use crate::attr::Serde; - attrs .iter() .filter(|a| a.path().is_ident("serde")) @@ -223,6 +219,20 @@ mod warning { writer.print(&buffer) } } +#[cfg(not(feature = "serde-compat"))] +mod warning { + use std::fmt::Display; + + // Just a stub! + #[allow(unused)] + pub fn print_warning( + title: impl Display, + content: impl Display, + note: impl Display, + ) -> std::io::Result<()> { + Ok(()) + } +} /// formats the generic arguments (like A, B in struct X{..}) as "" where x is a comma /// seperated list of generic arguments, or an empty string if there are no type generics (lifetime/const generics are ignored). diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index 3acd2b34..f15e186a 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -15,7 +15,7 @@ categories = [ "web-programming", ] readme = "../README.md" -rust-version = "1.75.0" +rust-version = "1.63.0" [features] chrono-impl = ["chrono"] diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 145f9d44..668e9dbf 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -29,10 +29,7 @@ mod recursive_export { use std::{any::TypeId, collections::HashSet, path::Path}; use super::export_into; - use crate::{ - typelist::{TypeList, TypeVisitor}, - Error, TS, - }; + use crate::{Error, TypeVisitor, TS}; /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute within the given /// base directory. @@ -79,7 +76,7 @@ mod recursive_export { out_dir, error: None, }; - T::dependency_types().for_each(&mut visitor); + T::visit_dependencies(&mut visitor); if let Some(e) = visitor.error { Err(e) diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 3e14cd46..58fdeab4 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -114,7 +114,7 @@ //! [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md) //! //! ## MSRV -//! The Minimum Supported Rust Version for this crate is 1.75.0 +//! The Minimum Supported Rust Version for this crate is 1.63.0 use std::{ any::TypeId, @@ -131,14 +131,12 @@ use std::{ pub use ts_rs_macros::TS; pub use crate::export::Error; -use crate::typelist::TypeList; #[cfg(feature = "chrono-impl")] mod chrono; mod export; #[cfg(feature = "serde-json-impl")] mod serde_json; -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. @@ -418,20 +416,19 @@ pub trait TS { /// This function will panic if the type cannot be inlined. fn inline() -> String; - /// Flatten an type declaration. + /// Flatten a type declaration. /// This function will panic if the type cannot be flattened. fn inline_flattened() -> String; - /// Returns a [`TypeList`] of all types on which this type depends. - fn dependency_types() -> impl TypeList + /// Iterates over all dependency of this type. + fn visit_dependencies(_: &mut impl TypeVisitor) where Self: 'static, { } - /// Returns a [`TypeList`] containing all generic parameters of this type. - /// If this type is not generic, this will return an empty [`TypeList`]. - fn generics() -> impl TypeList + /// Iterates over all type parameters of this type. + fn visit_generics(_: &mut impl TypeVisitor) where Self: 'static, { @@ -442,8 +439,6 @@ pub trait TS { where Self: 'static, { - use crate::typelist::TypeVisitor; - let mut deps: Vec = vec![]; struct Visit<'a>(&'a mut Vec); impl<'a> TypeVisitor for Visit<'a> { @@ -453,7 +448,7 @@ pub trait TS { } } } - Self::dependency_types().for_each(&mut Visit(&mut deps)); + Self::visit_dependencies(&mut Visit(&mut deps)); deps } @@ -530,7 +525,7 @@ pub trait TS { } /// Manually generate bindings for this type, returning a [`String`]. - /// This function does not format the output, even if the `format` feature is enabled. TODO + /// This function does not format the output, even if the `format` feature is enabled. /// /// # Automatic Exporting /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be @@ -578,6 +573,14 @@ pub trait TS { } } +/// A visitor used to iterate over all dependencies or generics of a type. +/// When an instance of [`TypeVisitor`] is passed to [`TS::visit_dependencies`] or +/// [`TS::visit_generics`], the [`TypeVisitor::visit`] method will be invoked for every dependency +/// or generic parameter respectively. +pub trait TypeVisitor: Sized { + fn visit(&mut self); +} + /// A typescript type which is depended upon by other types. /// This information is required for generating the correct import statements. #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] @@ -625,16 +628,19 @@ macro_rules! impl_tuples { impl<$($i: TS),*> TS for ($($i,)*) { type WithoutGenerics = (Dummy, ); fn name() -> String { - format!("[{}]", [$($i::name()),*].join(", ")) + format!("[{}]", [$(<$i as $crate::TS>::name()),*].join(", ")) } fn inline() -> String { panic!("tuple cannot be inlined!"); } - fn dependency_types() -> impl TypeList + fn visit_generics(v: &mut impl TypeVisitor) where Self: 'static { - ()$(.push::<$i>())* + $( + v.visit::<$i>(); + <$i>::visit_generics(v); + )* } fn inline_flattened() -> String { panic!("tuple cannot be flattened") } fn decl() -> String { panic!("tuple cannot be declared") } @@ -656,17 +662,19 @@ macro_rules! impl_wrapper { fn name() -> String { T::name() } fn inline() -> String { T::inline() } fn inline_flattened() -> String { T::inline_flattened() } - fn dependency_types() -> impl $crate::typelist::TypeList + fn visit_dependencies(v: &mut impl TypeVisitor) where - Self: 'static + Self: 'static, { - T::dependency_types() + T::visit_dependencies(v); } - fn generics() -> impl $crate::typelist::TypeList + + fn visit_generics(v: &mut impl TypeVisitor) where - Self: 'static + Self: 'static, { - ((std::marker::PhantomData::,), T::generics()) + T::visit_generics(v); + v.visit::(); } fn decl() -> String { panic!("wrapper type cannot be declared") } fn decl_concrete() -> String { panic!("wrapper type cannot be declared") } @@ -678,26 +686,26 @@ macro_rules! impl_wrapper { macro_rules! impl_shadow { (as $s:ty: $($impl:tt)*) => { $($impl)* { - type WithoutGenerics = <$s as TS>::WithoutGenerics; - fn ident() -> String { <$s>::ident() } - fn name() -> String { <$s>::name() } - fn inline() -> String { <$s>::inline() } - fn inline_flattened() -> String { <$s>::inline_flattened() } - fn dependency_types() -> impl $crate::typelist::TypeList + type WithoutGenerics = <$s as $crate::TS>::WithoutGenerics; + fn ident() -> String { <$s as $crate::TS>::ident() } + fn name() -> String { <$s as $crate::TS>::name() } + fn inline() -> String { <$s as $crate::TS>::inline() } + fn inline_flattened() -> String { <$s as $crate::TS>::inline_flattened() } + fn visit_dependencies(v: &mut impl $crate::TypeVisitor) where - Self: 'static + Self: 'static, { - <$s>::dependency_types() + <$s as $crate::TS>::visit_dependencies(v); } - fn generics() -> impl $crate::typelist::TypeList + fn visit_generics(v: &mut impl $crate::TypeVisitor) where - Self: 'static + Self: 'static, { - <$s>::generics() + <$s as $crate::TS>::visit_generics(v); } - fn decl() -> String { <$s>::decl() } - fn decl_concrete() -> String { <$s>::decl_concrete() } - fn output_path() -> Option<&'static std::path::Path> { <$s>::output_path() } + fn decl() -> String { <$s as $crate::TS>::decl() } + fn decl_concrete() -> String { <$s as $crate::TS>::decl_concrete() } + fn output_path() -> Option<&'static std::path::Path> { <$s as $crate::TS>::output_path() } } }; } @@ -713,18 +721,19 @@ impl TS for Option { format!("{} | null", T::inline()) } - fn dependency_types() -> impl TypeList + fn visit_dependencies(v: &mut impl TypeVisitor) where Self: 'static, { - T::dependency_types() + T::visit_dependencies(v); } - fn generics() -> impl TypeList + fn visit_generics(v: &mut impl TypeVisitor) where Self: 'static, { - T::generics().push::() + T::visit_generics(v); + v.visit::(); } fn decl() -> String { @@ -751,18 +760,22 @@ impl TS for Result { format!("{{ Ok : {} }} | {{ Err : {} }}", T::inline(), E::inline()) } - fn dependency_types() -> impl TypeList + fn visit_dependencies(v: &mut impl TypeVisitor) where Self: 'static, { - T::dependency_types().extend(E::dependency_types()) + T::visit_dependencies(v); + E::visit_dependencies(v); } - fn generics() -> impl TypeList + fn visit_generics(v: &mut impl TypeVisitor) where Self: 'static, { - T::generics().push::().extend(E::generics()).push::() + T::visit_generics(v); + v.visit::(); + E::visit_generics(v); + v.visit::(); } fn decl() -> String { @@ -793,18 +806,19 @@ impl TS for Vec { format!("Array<{}>", T::inline()) } - fn dependency_types() -> impl TypeList + fn visit_dependencies(v: &mut impl TypeVisitor) where Self: 'static, { - T::dependency_types() + T::visit_dependencies(v); } - fn generics() -> impl TypeList + fn visit_generics(v: &mut impl TypeVisitor) where Self: 'static, { - T::generics().push::() + T::visit_generics(v); + v.visit::(); } fn decl() -> String { @@ -846,18 +860,19 @@ impl TS for [T; N] { ) } - fn dependency_types() -> impl TypeList + fn visit_dependencies(v: &mut impl TypeVisitor) where Self: 'static, { - T::dependency_types() + T::visit_dependencies(v); } - fn generics() -> impl TypeList + fn visit_generics(v: &mut impl TypeVisitor) where Self: 'static, { - T::generics().push::() + T::visit_generics(v); + v.visit::(); } fn decl() -> String { @@ -888,18 +903,22 @@ impl TS for HashMap { format!("{{ [key: {}]: {} }}", K::inline(), V::inline()) } - fn dependency_types() -> impl TypeList + fn visit_dependencies(v: &mut impl TypeVisitor) where Self: 'static, { - K::dependency_types().extend(V::dependency_types()) + K::visit_dependencies(v); + V::visit_dependencies(v); } - fn generics() -> impl TypeList + fn visit_generics(v: &mut impl TypeVisitor) where Self: 'static, { - K::generics().push::().extend(V::generics()).push::() + K::visit_generics(v); + v.visit::(); + V::visit_generics(v); + v.visit::(); } fn decl() -> String { @@ -921,18 +940,19 @@ impl TS for Range { format!("{{ start: {}, end: {}, }}", I::name(), I::name()) } - fn dependency_types() -> impl TypeList + fn visit_dependencies(v: &mut impl TypeVisitor) where Self: 'static, { - I::dependency_types() + I::visit_dependencies(v); } - fn generics() -> impl TypeList + fn visit_generics(v: &mut impl TypeVisitor) where Self: 'static, { - I::generics().push::() + I::visit_generics(v); + v.visit::(); } fn decl() -> String { diff --git a/ts-rs/src/typelist.rs b/ts-rs/src/typelist.rs deleted file mode 100644 index 123bf521..00000000 --- a/ts-rs/src/typelist.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! A simple zero-sized collection of types. - -use std::{any::TypeId, marker::PhantomData}; - -use crate::TS; - -/// A visitor used to iterate over a [`TypeList`]. -/// -/// Example: -/// ``` -/// # use ts_rs::TS; -/// # use ts_rs::typelist::{TypeList, TypeVisitor}; -/// struct Visit; -/// impl TypeVisitor for Visit { -/// fn visit(&mut self) { -/// println!("{}", T::name()); -/// } -/// } -/// -/// # fn primitives() -> impl TypeList { -/// # let signed = ().push::().push::().push::().push::(); -/// # let unsigned = ().push::().push::().push::().push::(); -/// # ().push::() -/// # .push::() -/// # .extend(signed) -/// # .extend(unsigned) -/// # } -/// // fn primitives() -> impl TypeList { ... } -/// primitives().for_each(&mut Visit); -/// ``` -pub trait TypeVisitor: Sized { - fn visit(&mut self); -} - -/// A list containing types implementing `TS + 'static + ?Sized`. -/// -/// To construct a [`TypeList`], start with the empty list, which is the unit type `()`, and -/// repeatedly call [`TypeList::push`] or [`TypeList::extend`] on it. -/// -/// Example: -/// ``` -/// # use ts_rs::typelist::TypeList; -/// fn primitives() -> impl TypeList { -/// let signed = ().push::().push::().push::().push::(); -/// let unsigned = ().push::().push::().push::().push::(); -/// ().push::() -/// .push::() -/// .extend(signed) -/// .extend(unsigned) -/// } -/// ``` -/// -/// The only way to get access to the types contained in a [`TypeList`] is to iterate over it by -/// creating a visitor implementing [`TypeVisitor`] and calling [`TypeList::for_each`]. -/// -/// Under the hood, [`TypeList`] is recursively defined as follows: -/// - The unit type `()` is the empty [`TypeList`] -/// - For every `T: TS`, `(PhantomData,)` is a [`TypeList`] -/// - For every two [`TypeList`]s `A` and `B`, `(A, B)` is a [`TypeList`] -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/integration/issue_308.rs b/ts-rs/tests/integration/issue_308.rs index 6d3a04c9..3a3d915c 100644 --- a/ts-rs/tests/integration/issue_308.rs +++ b/ts-rs/tests/integration/issue_308.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use ts_rs::{typelist::TypeList, Dependency, Error, TS}; +use ts_rs::{Dependency, Error, TypeVisitor, TS}; #[rustfmt::skip] trait Malicious { @@ -13,9 +13,9 @@ trait Malicious { fn name() -> String { unimplemented!() } fn inline() -> String { unimplemented!() } fn inline_flattened() -> String { unimplemented!() } - fn dependency_types() -> impl TypeList {} - fn generics() -> impl TypeList {} fn dependencies() -> Vec { unimplemented!() } + fn visit_dependencies(_: &mut impl TypeVisitor) { unimplemented!() } + fn visit_generics(_: &mut impl TypeVisitor) { unimplemented!() } fn export() -> Result<(), Error> { unimplemented!() } fn export_all() -> Result<(), Error> { unimplemented!() } fn export_all_to(out_dir: impl AsRef) -> Result<(), Error> { unimplemented!() } diff --git a/ts-rs/tests/integration/issue_317.rs b/ts-rs/tests/integration/issue_317.rs new file mode 100644 index 00000000..fed7f886 --- /dev/null +++ b/ts-rs/tests/integration/issue_317.rs @@ -0,0 +1,18 @@ +use ts_rs::TS; + +#[derive(TS)] +#[ts(export_to = "issue_317/")] +struct VariantId(u32); + +#[derive(TS)] +#[ts(export_to = "issue_317/")] +struct VariantOverview { + id: u32, + name: String, +} + +#[derive(TS)] +#[ts(export, export_to = "issue_317/")] +struct Container { + variants: Vec<(VariantId, VariantOverview)>, +} diff --git a/ts-rs/tests/integration/main.rs b/ts-rs/tests/integration/main.rs index 971d02fb..e95f6350 100644 --- a/ts-rs/tests/integration/main.rs +++ b/ts-rs/tests/integration/main.rs @@ -23,6 +23,7 @@ mod infer_as; mod issue_168; mod issue_232; mod issue_308; +mod issue_317; mod issue_70; mod issue_80; mod leading_colon; diff --git a/ts-rs/tests/integration/recursion_limit.rs b/ts-rs/tests/integration/recursion_limit.rs index 5406120c..16e1a2d2 100644 --- a/ts-rs/tests/integration/recursion_limit.rs +++ b/ts-rs/tests/integration/recursion_limit.rs @@ -1,9 +1,6 @@ use std::any::TypeId; -use ts_rs::{ - typelist::{TypeList, TypeVisitor}, - TS, -}; +use ts_rs::{TypeVisitor, TS}; #[rustfmt::skip] #[allow(clippy::all)] @@ -77,7 +74,36 @@ fn very_big_enum() { } let mut visitor = Visitor(false); - VeryBigEnum::dependency_types().for_each(&mut visitor); + VeryBigEnum::visit_dependencies(&mut visitor); assert!(visitor.0, "there must be at least one dependency"); } + +macro_rules! generate_types { + ($a:ident, $b:ident $($t:tt)*) => { + #[derive(TS)] + #[ts(export, export_to = "very_big_types/")] + struct $a($b); + generate_types!($b $($t)*); + }; + ($a:ident) => { + #[derive(TS)] + #[ts(export, export_to = "very_big_types/")] + struct $a; + } +} + +// This generates +// `#[derive(TS)] struct T000(T001)` +// `#[derive(TS)] struct T001(T002)` +// ... +// `#[derive(TS)] struct T082(T083)` +// `#[derive(TS)] struct T083;` +generate_types!( + T000, T001, T002, T003, T004, T005, T006, T007, T008, T009, T010, T011, T012, T013, T014, T015, + T016, T017, T018, T019, T020, T021, T022, T023, T024, T025, T026, T027, T028, T029, T030, T031, + T032, T033, T034, T035, T036, T037, T038, T039, T040, T041, T042, T043, T044, T045, T046, T047, + T048, T049, T050, T051, T052, T053, T054, T055, T056, T057, T058, T059, T060, T061, T062, T063, + T064, T065, T066, T067, T068, T069, T070, T071, T072, T073, T074, T075, T076, T077, T078, T079, + T080, T081, T082, T083 +); diff --git a/ts-rs/tests/integration/serde_skip_with_default.rs b/ts-rs/tests/integration/serde_skip_with_default.rs index b67e2e58..66600592 100644 --- a/ts-rs/tests/integration/serde_skip_with_default.rs +++ b/ts-rs/tests/integration/serde_skip_with_default.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "serde-compat")] #![allow(dead_code)] // from issue #107. This does now no longer generate a warning. diff --git a/ts-rs/tests/integration/struct_rename.rs b/ts-rs/tests/integration/struct_rename.rs index 9536fcaa..9976a5a4 100644 --- a/ts-rs/tests/integration/struct_rename.rs +++ b/ts-rs/tests/integration/struct_rename.rs @@ -59,10 +59,6 @@ struct RenameAllScreamingKebab { #[test] fn rename_all_screaming_kebab_case() { let rename_all = RenameAllScreamingKebab::default(); - assert_eq!( - serde_json::to_string(&rename_all).unwrap(), - r#"{"CRC32C-HASH":0,"SOME-FIELD":0,"SOME-OTHER-FIELD":0}"# - ); assert_eq!( RenameAllScreamingKebab::inline(), r#"{ "CRC32C-HASH": number, "SOME-FIELD": number, "SOME-OTHER-FIELD": number, }"# diff --git a/ts-rs/tests/integration/tuple.rs b/ts-rs/tests/integration/tuple.rs index 88ecef61..9d85c919 100644 --- a/ts-rs/tests/integration/tuple.rs +++ b/ts-rs/tests/integration/tuple.rs @@ -23,12 +23,65 @@ fn test_newtype() { assert_eq!("type NewType = string;", NewType::decl()); } +#[derive(TS)] +#[ts(export, export_to = "tuple/")] +struct TupleNewType(String, i32, (i32, i32)); + #[test] fn test_tuple_newtype() { - #[derive(TS)] - struct TupleNewType(String, i32, (i32, i32)); assert_eq!( "type TupleNewType = [string, number, [number, number]];", TupleNewType::decl() ) } + +#[derive(TS)] +#[ts(export, export_to = "tuple/")] +struct Dep1; + +#[derive(TS)] +#[ts(export, export_to = "tuple/")] +struct Dep2; + +#[derive(TS)] +#[ts(export, export_to = "tuple/")] +struct Dep3; + +#[derive(TS)] +#[ts(export, export_to = "tuple/")] +struct Dep4 { + a: (T, T), + b: (T, T), +} + +#[derive(TS)] +#[ts(export, export_to = "tuple/")] +struct TupleWithDependencies(Dep1, Dep2, Dep4); + +#[test] +fn tuple_with_dependencies() { + assert_eq!( + "type TupleWithDependencies = [Dep1, Dep2, Dep4];", + TupleWithDependencies::decl() + ); +} + +#[derive(TS)] +#[ts(export, export_to = "tuple/")] +struct StructWithTuples { + a: (Dep1, Dep1), + b: (Dep2, Dep2), + c: (Dep4, Dep4), +} + +#[test] +fn struct_with_tuples() { + assert_eq!( + "type StructWithTuples = { \ + a: [Dep1, Dep1], \ + b: [Dep2, Dep2], \ + c: [Dep4, Dep4], \ + };", + StructWithTuples::decl() + ); +} From 939041e120a5ddf12c62de4b3a2a6642c3d4c8ef Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 20 May 2024 08:51:02 -0300 Subject: [PATCH 10/28] Remove duplicate declaration --- ts-rs/tests/integration/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/ts-rs/tests/integration/main.rs b/ts-rs/tests/integration/main.rs index dd0d1930..e95f6350 100644 --- a/ts-rs/tests/integration/main.rs +++ b/ts-rs/tests/integration/main.rs @@ -61,4 +61,3 @@ mod union_with_data; mod union_with_internal_tag; mod unit; mod r#unsized; -mod issue_317; From a3ed6d8cff99115098219593d1cb53af4ce91d5f Mon Sep 17 00:00:00 2001 From: Gustavo Date: Wed, 22 May 2024 08:50:15 -0300 Subject: [PATCH 11/28] Reduce amount of syscalls and memory allocations --- ts-rs/src/export.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 668e9dbf..f96e95da 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -144,24 +144,31 @@ pub(crate) fn export_to>(path: P) -> Re file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; - let mut buf = String::new(); - file.read_to_string(&mut buf)?; + let content_len = usize::try_from(file.metadata()?.len()).unwrap() - NOTE.len(); + let mut original_contents = String::with_capacity(content_len); + file.read_to_string(&mut original_contents)?; let imports = imports .lines() - .filter(|x| !buf.contains(x)) + .filter(|x| !original_contents.contains(x)) .collect::(); file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; - file.write_all(imports.as_bytes())?; + let buffer_size = + imports.as_bytes().len() + decl.as_bytes().len() + content_len + 3; + + let mut buffer = String::with_capacity(buffer_size); + + buffer.push_str(&imports); if !imports.is_empty() { - file.write_all(b"\n")?; + buffer.push('\n'); } - file.write_all(buf.as_bytes())?; + buffer.push_str(&original_contents); + buffer.push_str("\n\n"); + buffer.push_str(decl); - file.write_all(b"\n\n")?; - file.write_all(decl.as_bytes())?; + file.write_all(buffer.as_bytes())?; entry.insert(type_name); } From 121ac377a8961c1d438e4f99e1770f6e0d3df2fd Mon Sep 17 00:00:00 2001 From: Gustavo Date: Wed, 22 May 2024 08:56:09 -0300 Subject: [PATCH 12/28] Replace negated condition with guard clause --- ts-rs/src/export.rs | 67 +++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index f96e95da..44942138 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -129,49 +129,50 @@ pub(crate) fn export_to>(path: P) -> Re let mut lock = EXPORT_PATHS.lock().unwrap(); if let Some(entry) = lock.get_mut(&path) { - if !entry.contains(&type_name) { - let (header, decl) = buffer.split_once("\n\n").unwrap(); - let imports = if header.len() > NOTE.len() { - &header[NOTE.len()..] - } else { - "" - }; - - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&path)?; + if entry.contains(&type_name) { + return Ok(()); + } - file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; + let (header, decl) = buffer.split_once("\n\n").unwrap(); + let imports = if header.len() > NOTE.len() { + &header[NOTE.len()..] + } else { + "" + }; - let content_len = usize::try_from(file.metadata()?.len()).unwrap() - NOTE.len(); - let mut original_contents = String::with_capacity(content_len); - file.read_to_string(&mut original_contents)?; + let mut file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&path)?; - let imports = imports - .lines() - .filter(|x| !original_contents.contains(x)) - .collect::(); + file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; - file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; + let content_len = usize::try_from(file.metadata()?.len()).unwrap() - NOTE.len(); + let mut original_contents = String::with_capacity(content_len); + file.read_to_string(&mut original_contents)?; - let buffer_size = - imports.as_bytes().len() + decl.as_bytes().len() + content_len + 3; + let imports = imports + .lines() + .filter(|x| !original_contents.contains(x)) + .collect::(); - let mut buffer = String::with_capacity(buffer_size); + file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; - buffer.push_str(&imports); - if !imports.is_empty() { - buffer.push('\n'); - } - buffer.push_str(&original_contents); - buffer.push_str("\n\n"); - buffer.push_str(decl); + let buffer_size = imports.as_bytes().len() + decl.as_bytes().len() + content_len + 3; - file.write_all(buffer.as_bytes())?; + let mut buffer = String::with_capacity(buffer_size); - entry.insert(type_name); + buffer.push_str(&imports); + if !imports.is_empty() { + buffer.push('\n'); } + buffer.push_str(&original_contents); + buffer.push_str("\n\n"); + buffer.push_str(decl); + + file.write_all(buffer.as_bytes())?; + + entry.insert(type_name); } else { let mut file = File::create(&path)?; file.write_all(buffer.as_bytes())?; From 8e3e59fee0b0cc663ae8c2b3e4faae2bbb7e1b88 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:30:19 -0300 Subject: [PATCH 13/28] Fix compiler error --- ts-rs/src/export/path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts-rs/src/export/path.rs b/ts-rs/src/export/path.rs index d17034fa..447bf1ab 100644 --- a/ts-rs/src/export/path.rs +++ b/ts-rs/src/export/path.rs @@ -4,7 +4,7 @@ use super::Error; const ERROR_MESSAGE: &str = r#"The path provided with `#[ts(export_to = "..")]` is not valid"#; -pub fn absolute>(path: T) -> Result { +pub fn absolute>(path: T) -> Result { let path = std::env::current_dir()?.join(path.as_ref()); From b80240eb47ffaf1e434056904b3da3736cfbfa8f Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 12:58:25 -0300 Subject: [PATCH 14/28] Fix double blank line --- ts-rs/src/export.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 3270fe3d..59423f14 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -158,7 +158,7 @@ pub(crate) fn export_to>(path: P) -> Re file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; - let buffer_size = imports.as_bytes().len() + decl.as_bytes().len() + content_len + 3; + let buffer_size = imports.len() + decl.len() + content_len + 3; let mut buffer = String::with_capacity(buffer_size); @@ -167,7 +167,7 @@ pub(crate) fn export_to>(path: P) -> Re buffer.push('\n'); } buffer.push_str(&original_contents); - buffer.push_str("\n\n"); + buffer.push('\n'); buffer.push_str(decl); file.write_all(buffer.as_bytes())?; From 4344480003f4245225167c20b6eb7f08673f6cb5 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 13:08:15 -0300 Subject: [PATCH 15/28] Make test more robust by checking if all exports are in file --- ts-rs/tests/integration/same_file_export.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/ts-rs/tests/integration/same_file_export.rs b/ts-rs/tests/integration/same_file_export.rs index 3a9eaeec..6ec35273 100644 --- a/ts-rs/tests/integration/same_file_export.rs +++ b/ts-rs/tests/integration/same_file_export.rs @@ -13,21 +13,34 @@ struct DepB { } #[derive(TS)] -#[ts(export, export_to = "same_file_export/types.ts")] +#[ts(export_to = "same_file_export/types.ts")] struct A { foo: DepA, } #[derive(TS)] -#[ts(export, export_to = "same_file_export/types.ts")] +#[ts(export_to = "same_file_export/types.ts")] struct B { foo: DepB, } #[derive(TS)] -#[ts(export, export_to = "same_file_export/types.ts")] +#[ts(export_to = "same_file_export/types.ts")] struct C { foo: DepA, bar: DepB, biz: B, } + +#[test] +fn test() { + A::export_all().unwrap(); + B::export_all().unwrap(); + C::export_all().unwrap(); + + let contents = std::fs::read_to_string(&A::default_output_path().unwrap()).unwrap(); + + assert!(contents.contains(&A::decl())); + assert!(contents.contains(&B::decl())); + assert!(contents.contains(&C::decl())); +} From 6a962c07ac8819f94f045ca865f94fb51779cd5e Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 13:09:50 -0300 Subject: [PATCH 16/28] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c973c5a..2d94c06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - The `bson-uuid-impl` feature now supports `bson::oid::ObjectId` as well +- Allow multile types to have the same `#[ts(export_to = "...")]` attribute and be exported to the same file ### Fixes From a5a3fbaa5f622b0d5cbd59748b847269b21cd344 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 13:12:01 -0300 Subject: [PATCH 17/28] Failure report --- ts-rs/tests/integration/same_file_export.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ts-rs/tests/integration/same_file_export.rs b/ts-rs/tests/integration/same_file_export.rs index 6ec35273..0d42f969 100644 --- a/ts-rs/tests/integration/same_file_export.rs +++ b/ts-rs/tests/integration/same_file_export.rs @@ -40,7 +40,7 @@ fn test() { let contents = std::fs::read_to_string(&A::default_output_path().unwrap()).unwrap(); - assert!(contents.contains(&A::decl())); - assert!(contents.contains(&B::decl())); - assert!(contents.contains(&C::decl())); + assert!(contents.contains(&A::decl()), "{contents}"); + assert!(contents.contains(&B::decl()), "{contents}"); + assert!(contents.contains(&C::decl()), "{contents}"); } From cd37d20ec972ac796121204ec7293627afddc847 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 13:15:37 -0300 Subject: [PATCH 18/28] Account for the format feature --- ts-rs/tests/integration/same_file_export.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ts-rs/tests/integration/same_file_export.rs b/ts-rs/tests/integration/same_file_export.rs index 0d42f969..48ba0a31 100644 --- a/ts-rs/tests/integration/same_file_export.rs +++ b/ts-rs/tests/integration/same_file_export.rs @@ -33,14 +33,20 @@ struct C { } #[test] -fn test() { +fn all_types_exported() { A::export_all().unwrap(); B::export_all().unwrap(); C::export_all().unwrap(); let contents = std::fs::read_to_string(&A::default_output_path().unwrap()).unwrap(); - assert!(contents.contains(&A::decl()), "{contents}"); - assert!(contents.contains(&B::decl()), "{contents}"); - assert!(contents.contains(&C::decl()), "{contents}"); + if cfg!(feature = "format") { + assert!(contents.contains("export type A = { foo: DepA }")); + assert!(contents.contains("export type B = { foo: DepB }")); + assert!(contents.contains("export type C = { foo: DepA; bar: DepB; biz: B }")); + } else { + assert!(contents.contains(&A::decl())); + assert!(contents.contains(&B::decl())); + assert!(contents.contains(&C::decl())); + } } From 04c1ec4f2c112bdf720053358edb584e5176b350 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 15:41:32 -0300 Subject: [PATCH 19/28] Make type order deterministic --- ts-rs/src/export.rs | 136 +++++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 45 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 59423f14..251fe325 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -1,7 +1,7 @@ use std::{ any::TypeId, borrow::Cow, - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, fmt::Write, fs::File, io::{Seek, SeekFrom}, @@ -123,68 +123,114 @@ pub(crate) fn export_to>(path: P) -> Re std::fs::create_dir_all(parent)?; } - { - use std::io::{Read, Write}; + export_and_merge(path, type_name, buffer)?; - let mut lock = EXPORT_PATHS.lock().unwrap(); + Ok(()) +} - if let Some(entry) = lock.get_mut(&path) { - if entry.contains(&type_name) { - return Ok(()); - } +/// Exports the type to a new file if the file hasn't yet been written to. +/// Otherwise, finds its place in the already existing file and inserts it. +fn export_and_merge(path: PathBuf, type_name: String, generated_type: String) -> Result<(), Error> { + use std::io::{Read, Write}; - let (header, decl) = buffer.split_once("\n\n").unwrap(); - let imports = if header.len() > NOTE.len() { - &header[NOTE.len()..] - } else { - "" - }; + let mut lock = EXPORT_PATHS.lock().unwrap(); - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&path)?; + let Some(entry) = lock.get_mut(&path) else { + // The file hasn't been written to yet, so it must be + // overwritten + let mut file = File::create(&path)?; + file.write_all(generated_type.as_bytes())?; + file.sync_all()?; - file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; + let mut set = HashSet::new(); + set.insert(type_name); + lock.insert(path, set); - let content_len = usize::try_from(file.metadata()?.len()).unwrap() - NOTE.len(); - let mut original_contents = String::with_capacity(content_len); - file.read_to_string(&mut original_contents)?; + return Ok(()); + }; - let imports = imports - .lines() - .filter(|x| !original_contents.contains(x)) - .collect::(); + if entry.contains(&type_name) { + return Ok(()); + } - file.seek(SeekFrom::Start(NOTE.len().try_into().unwrap()))?; + let mut file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&path)?; - let buffer_size = imports.len() + decl.len() + content_len + 3; + let file_len = file.metadata()?.len() as usize; - let mut buffer = String::with_capacity(buffer_size); + let mut original_contents = String::with_capacity(file_len); + file.read_to_string(&mut original_contents)?; - buffer.push_str(&imports); - if !imports.is_empty() { - buffer.push('\n'); - } - buffer.push_str(&original_contents); - buffer.push('\n'); - buffer.push_str(decl); + let buffer = merge(original_contents, generated_type); + + file.seek(SeekFrom::Start(NOTE.len() as u64))?; + + file.write_all(buffer.as_bytes())?; + file.sync_all()?; + + entry.insert(type_name); + + Ok(()) +} + +const HEADER_ERROR_MESSAGE: &'static str = "The generated strings must have their NOTE and imports separated from their type declarations by a new line"; + +/// Inserts the imports and declaration from the newly generated type +/// into the contents of the file, removimg duplicate imports and organazing +/// both imports and declarations alphabetically +fn merge(original_contents: String, new_contents: String) -> String { + let (original_header, original_decls) = original_contents + .split_once("\n\n") + .expect(HEADER_ERROR_MESSAGE); + let (new_header, new_decl) = new_contents.split_once("\n\n").expect(HEADER_ERROR_MESSAGE); - file.write_all(buffer.as_bytes())?; + let imports = original_header + .trim_start_matches(NOTE) + .lines() + .chain(new_header.trim_start_matches(NOTE).lines()) + .collect::>(); - entry.insert(type_name); + let import_len = imports.iter().map(|&x| x.len()).sum::() + imports.len(); + let capacity = import_len + original_decls.len() + new_decl.len() + 2; + + let mut buffer = String::with_capacity(capacity); + + for import in imports { + buffer.push_str(import); + buffer.push('\n') + } + + let new_decl = new_decl.trim_matches('\n'); + let original_decls = original_decls.split("\n\n").map(|x| x.trim_matches('\n')); + + let mut inserted = false; + for decl in original_decls { + if inserted || decl < new_decl { + buffer.push('\n'); + buffer.push_str(decl); + buffer.push('\n'); } else { - let mut file = File::create(&path)?; - file.write_all(buffer.as_bytes())?; - file.sync_data()?; + buffer.push('\n'); + buffer.push_str(new_decl); + buffer.push('\n'); + + buffer.push('\n'); + buffer.push_str(decl); + buffer.push('\n'); - let mut set = HashSet::new(); - set.insert(type_name); - lock.insert(path, set); + inserted = true; } } - Ok(()) + if !inserted { + buffer.push('\n'); + buffer.push_str(new_decl); + buffer.push('\n'); + } + + buffer } /// Returns the generated definition for `T`. From 9db2c1c6a413cb5565106912e8dd25ce1a403a59 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 19:34:07 -0300 Subject: [PATCH 20/28] Ignore JSDoc when sorting declarations --- ts-rs/src/export.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 251fe325..0b5fa509 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -203,11 +203,13 @@ fn merge(original_contents: String, new_contents: String) -> String { } let new_decl = new_decl.trim_matches('\n'); + let new_decl_name = &new_decl[new_decl.find("export type").unwrap()..]; let original_decls = original_decls.split("\n\n").map(|x| x.trim_matches('\n')); let mut inserted = false; for decl in original_decls { - if inserted || decl < new_decl { + let decl_name = &decl[decl.find("export type").unwrap()..]; + if inserted || decl_name < new_decl_name { buffer.push('\n'); buffer.push_str(decl); buffer.push('\n'); From 419044f03c7caf13b7177fc021c293b74ae998b9 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 19:39:58 -0300 Subject: [PATCH 21/28] Only consider type name when sorting declarations --- ts-rs/src/export.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 0b5fa509..4b329005 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -177,6 +177,8 @@ fn export_and_merge(path: PathBuf, type_name: String, generated_type: String) -> const HEADER_ERROR_MESSAGE: &'static str = "The generated strings must have their NOTE and imports separated from their type declarations by a new line"; +const DECLARATION_START: &'static str = "export type "; + /// Inserts the imports and declaration from the newly generated type /// into the contents of the file, removimg duplicate imports and organazing /// both imports and declarations alphabetically @@ -203,12 +205,19 @@ fn merge(original_contents: String, new_contents: String) -> String { } let new_decl = new_decl.trim_matches('\n'); - let new_decl_name = &new_decl[new_decl.find("export type").unwrap()..]; + + let new_decl_start = new_decl.find(DECLARATION_START).unwrap() + DECLARATION_START.len(); + let new_decl_end = new_decl[new_decl_start..].find(' ').unwrap(); + let new_decl_name = &new_decl[new_decl_start..new_decl_end]; + let original_decls = original_decls.split("\n\n").map(|x| x.trim_matches('\n')); let mut inserted = false; for decl in original_decls { - let decl_name = &decl[decl.find("export type").unwrap()..]; + let decl_start = decl.find(DECLARATION_START).unwrap() + DECLARATION_START.len(); + let decl_end = decl[decl_start..].find(' ').unwrap(); + let decl_name = &decl[decl_start..decl_end]; + if inserted || decl_name < new_decl_name { buffer.push('\n'); buffer.push_str(decl); From 8a43254f1609c7f92fb2a160ab3bbb39a4c554bd Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 19:46:37 -0300 Subject: [PATCH 22/28] Fix slice index --- ts-rs/src/export.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 4b329005..0781d7c6 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -207,7 +207,7 @@ fn merge(original_contents: String, new_contents: String) -> String { let new_decl = new_decl.trim_matches('\n'); let new_decl_start = new_decl.find(DECLARATION_START).unwrap() + DECLARATION_START.len(); - let new_decl_end = new_decl[new_decl_start..].find(' ').unwrap(); + let new_decl_end = new_decl_start + new_decl[new_decl_start..].find(' ').unwrap(); let new_decl_name = &new_decl[new_decl_start..new_decl_end]; let original_decls = original_decls.split("\n\n").map(|x| x.trim_matches('\n')); @@ -215,7 +215,7 @@ fn merge(original_contents: String, new_contents: String) -> String { let mut inserted = false; for decl in original_decls { let decl_start = decl.find(DECLARATION_START).unwrap() + DECLARATION_START.len(); - let decl_end = decl[decl_start..].find(' ').unwrap(); + let decl_end = decl_start + decl[decl_start..].find(' ').unwrap(); let decl_name = &decl[decl_start..decl_end]; if inserted || decl_name < new_decl_name { From bc108074e017e106331264fe65f9f8df4763d2d1 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 19:56:24 -0300 Subject: [PATCH 23/28] Get type name without index math --- ts-rs/src/export.rs | 20 ++++++++++++----- ts-rs/tests/integration/same_file_export.rs | 25 +++------------------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 0781d7c6..d2f4b216 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -206,17 +206,25 @@ fn merge(original_contents: String, new_contents: String) -> String { let new_decl = new_decl.trim_matches('\n'); - let new_decl_start = new_decl.find(DECLARATION_START).unwrap() + DECLARATION_START.len(); - let new_decl_end = new_decl_start + new_decl[new_decl_start..].find(' ').unwrap(); - let new_decl_name = &new_decl[new_decl_start..new_decl_end]; + let new_decl_name = new_decl + .split(DECLARATION_START) + .nth(1) + .unwrap() + .split_whitespace() + .next() + .unwrap(); let original_decls = original_decls.split("\n\n").map(|x| x.trim_matches('\n')); let mut inserted = false; for decl in original_decls { - let decl_start = decl.find(DECLARATION_START).unwrap() + DECLARATION_START.len(); - let decl_end = decl_start + decl[decl_start..].find(' ').unwrap(); - let decl_name = &decl[decl_start..decl_end]; + let decl_name = decl + .split(DECLARATION_START) + .nth(1) + .unwrap() + .split_whitespace() + .next() + .unwrap(); if inserted || decl_name < new_decl_name { buffer.push('\n'); diff --git a/ts-rs/tests/integration/same_file_export.rs b/ts-rs/tests/integration/same_file_export.rs index 48ba0a31..3a9eaeec 100644 --- a/ts-rs/tests/integration/same_file_export.rs +++ b/ts-rs/tests/integration/same_file_export.rs @@ -13,40 +13,21 @@ struct DepB { } #[derive(TS)] -#[ts(export_to = "same_file_export/types.ts")] +#[ts(export, export_to = "same_file_export/types.ts")] struct A { foo: DepA, } #[derive(TS)] -#[ts(export_to = "same_file_export/types.ts")] +#[ts(export, export_to = "same_file_export/types.ts")] struct B { foo: DepB, } #[derive(TS)] -#[ts(export_to = "same_file_export/types.ts")] +#[ts(export, export_to = "same_file_export/types.ts")] struct C { foo: DepA, bar: DepB, biz: B, } - -#[test] -fn all_types_exported() { - A::export_all().unwrap(); - B::export_all().unwrap(); - C::export_all().unwrap(); - - let contents = std::fs::read_to_string(&A::default_output_path().unwrap()).unwrap(); - - if cfg!(feature = "format") { - assert!(contents.contains("export type A = { foo: DepA }")); - assert!(contents.contains("export type B = { foo: DepB }")); - assert!(contents.contains("export type C = { foo: DepA; bar: DepB; biz: B }")); - } else { - assert!(contents.contains(&A::decl())); - assert!(contents.contains(&B::decl())); - assert!(contents.contains(&C::decl())); - } -} From 25434df9ddf3f06afd9be03545e0a9cffec8e552 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 19:59:43 -0300 Subject: [PATCH 24/28] Attempt to fix duplicated NOTE --- ts-rs/src/export.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index d2f4b216..00183c38 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -165,7 +165,7 @@ fn export_and_merge(path: PathBuf, type_name: String, generated_type: String) -> let buffer = merge(original_contents, generated_type); - file.seek(SeekFrom::Start(NOTE.len() as u64))?; + file.seek(SeekFrom::Start(0))?; file.write_all(buffer.as_bytes())?; file.sync_all()?; @@ -195,10 +195,12 @@ fn merge(original_contents: String, new_contents: String) -> String { .collect::>(); let import_len = imports.iter().map(|&x| x.len()).sum::() + imports.len(); - let capacity = import_len + original_decls.len() + new_decl.len() + 2; + let capacity = NOTE.len() + import_len + original_decls.len() + new_decl.len() + 2; let mut buffer = String::with_capacity(capacity); + buffer.push_str(NOTE); + for import in imports { buffer.push_str(import); buffer.push('\n') From dedfba53a64c0ef2ff9cbd296049ea99e93ae94c Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Wed, 24 Jul 2024 20:03:16 -0300 Subject: [PATCH 25/28] Guarantee that the type name will be used --- ts-rs/src/export.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 00183c38..99e5476e 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -210,7 +210,7 @@ fn merge(original_contents: String, new_contents: String) -> String { let new_decl_name = new_decl .split(DECLARATION_START) - .nth(1) + .last() .unwrap() .split_whitespace() .next() @@ -222,7 +222,7 @@ fn merge(original_contents: String, new_contents: String) -> String { for decl in original_decls { let decl_name = decl .split(DECLARATION_START) - .nth(1) + .last() .unwrap() .split_whitespace() .next() From ad1b46ed1700a7e2981c1c75c4558b4d954138ba Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Thu, 25 Jul 2024 10:20:35 -0300 Subject: [PATCH 26/28] Fix double header error --- ts-rs/src/export.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 99e5476e..03374d81 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -158,9 +158,9 @@ fn export_and_merge(path: PathBuf, type_name: String, generated_type: String) -> .write(true) .open(&path)?; - let file_len = file.metadata()?.len() as usize; + let file_len = file.metadata()?.len(); - let mut original_contents = String::with_capacity(file_len); + let mut original_contents = String::with_capacity(file_len as usize); file.read_to_string(&mut original_contents)?; let buffer = merge(original_contents, generated_type); @@ -189,9 +189,9 @@ fn merge(original_contents: String, new_contents: String) -> String { let (new_header, new_decl) = new_contents.split_once("\n\n").expect(HEADER_ERROR_MESSAGE); let imports = original_header - .trim_start_matches(NOTE) .lines() - .chain(new_header.trim_start_matches(NOTE).lines()) + .skip(1) + .chain(new_header.lines().skip(1)) .collect::>(); let import_len = imports.iter().map(|&x| x.len()).sum::() + imports.len(); From d124d7bdd69f3c233aa1c3f7437d9b1fa95a4bd6 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Thu, 25 Jul 2024 11:19:06 -0300 Subject: [PATCH 27/28] Allocate less space for buffer by having the note from the original file be reused --- ts-rs/src/export.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 03374d81..bf2bf660 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -165,7 +165,7 @@ fn export_and_merge(path: PathBuf, type_name: String, generated_type: String) -> let buffer = merge(original_contents, generated_type); - file.seek(SeekFrom::Start(0))?; + file.seek(SeekFrom::Start(NOTE.len() as u64))?; file.write_all(buffer.as_bytes())?; file.sync_all()?; @@ -195,12 +195,10 @@ fn merge(original_contents: String, new_contents: String) -> String { .collect::>(); let import_len = imports.iter().map(|&x| x.len()).sum::() + imports.len(); - let capacity = NOTE.len() + import_len + original_decls.len() + new_decl.len() + 2; + let capacity = import_len + original_decls.len() + new_decl.len() + 2; let mut buffer = String::with_capacity(capacity); - buffer.push_str(NOTE); - for import in imports { buffer.push_str(import); buffer.push('\n') From 537437097521de62b739448cf956019a8e935f8c Mon Sep 17 00:00:00 2001 From: gustavo-shigueo Date: Thu, 1 Aug 2024 18:09:52 -0300 Subject: [PATCH 28/28] Revert rename to avoid conflicts with other branches --- macros/src/attr/field.rs | 550 +++++++++++++-------------- ts-rs/src/export.rs | 34 +- ts-rs/src/export/error.rs | 2 +- ts-rs/src/export/path.rs | 9 +- ts-rs/src/lib.rs | 12 +- ts-rs/tests/integration/issue_308.rs | 10 +- 6 files changed, 311 insertions(+), 306 deletions(-) diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index dde475d4..c7726efc 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -1,275 +1,275 @@ -use syn::{ - AngleBracketedGenericArguments, Attribute, Field, GenericArgument, Ident, PathArguments, QSelf, - Result, ReturnType, Type, TypeArray, TypeGroup, TypeParen, TypePath, TypePtr, TypeReference, - TypeSlice, TypeTuple, -}; - -use super::{parse_assign_from_str, parse_assign_str, Attr, Serde}; -use crate::utils::{parse_attrs, parse_docs}; - -#[derive(Default)] -pub struct FieldAttr { - type_as: Option, - pub type_override: Option, - pub rename: Option, - pub inline: bool, - pub skip: bool, - pub optional: Optional, - pub flatten: bool, - pub docs: String, - - pub using_serde_with: bool, -} - -/// Indicates whether the field is marked with `#[ts(optional)]`. -/// `#[ts(optional)]` turns an `t: Option` into `t?: T`, while -/// `#[ts(optional = nullable)]` turns it into `t?: T | null`. -#[derive(Default)] -pub struct Optional { - pub optional: bool, - pub nullable: bool, -} - -impl FieldAttr { - pub fn from_attrs(attrs: &[Attribute]) -> Result { - let mut result = parse_attrs::(attrs)?; - - if cfg!(feature = "serde-compat") && !result.skip { - let serde_attr = crate::utils::parse_serde_attrs::(attrs); - result = result.merge(serde_attr.0); - } - - result.docs = parse_docs(attrs)?; - - Ok(result) - } - - pub fn type_as(&self, original_type: &Type) -> Type { - if let Some(mut ty) = self.type_as.clone() { - replace_underscore(&mut ty, original_type); - ty - } else { - original_type.clone() - } - } -} - -impl Attr for FieldAttr { - type Item = Field; - - fn merge(self, other: Self) -> Self { - Self { - type_as: self.type_as.or(other.type_as), - type_override: self.type_override.or(other.type_override), - rename: self.rename.or(other.rename), - inline: self.inline || other.inline, - skip: self.skip || other.skip, - optional: Optional { - optional: self.optional.optional || other.optional.optional, - nullable: self.optional.nullable || other.optional.nullable, - }, - flatten: self.flatten || other.flatten, - - using_serde_with: self.using_serde_with || other.using_serde_with, - - // We can't emit TSDoc for a flattened field - // and we cant make this invalid in assert_validity because - // this documentation is totally valid in Rust - docs: if self.flatten || other.flatten { - String::new() - } else { - self.docs + &other.docs - }, - } - } - - fn assert_validity(&self, field: &Self::Item) -> Result<()> { - if cfg!(feature = "serde-compat") - && self.using_serde_with - && !(self.type_as.is_some() || self.type_override.is_some()) - { - syn_err_spanned!( - field; - r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"# - ) - } - - if self.type_override.is_some() { - if self.type_as.is_some() { - syn_err_spanned!(field; "`type` is not compatible with `as`") - } - - if self.inline { - syn_err_spanned!(field; "`type` is not compatible with `inline`") - } - - if self.flatten { - syn_err_spanned!( - field; - "`type` is not compatible with `flatten`" - ); - } - } - - if self.flatten { - if self.type_as.is_some() { - syn_err_spanned!( - field; - "`as` is not compatible with `flatten`" - ); - } - - if self.rename.is_some() { - syn_err_spanned!( - field; - "`rename` is not compatible with `flatten`" - ); - } - - if self.inline { - syn_err_spanned!( - field; - "`inline` is not compatible with `flatten`" - ); - } - - if self.optional.optional { - syn_err_spanned!( - field; - "`optional` is not compatible with `flatten`" - ); - } - } - - if field.ident.is_none() { - if self.flatten { - syn_err_spanned!( - field; - "`flatten` cannot with tuple struct fields" - ); - } - - if self.rename.is_some() { - syn_err_spanned!( - field; - "`flatten` cannot with tuple struct fields" - ); - } - - if self.optional.optional { - syn_err_spanned!( - field; - "`optional` cannot with tuple struct fields" - ); - } - } - - Ok(()) - } -} - -impl_parse! { - FieldAttr(input, out) { - "as" => out.type_as = Some(parse_assign_from_str(input)?), - "type" => out.type_override = Some(parse_assign_str(input)?), - "rename" => out.rename = Some(parse_assign_str(input)?), - "inline" => out.inline = true, - "skip" => out.skip = true, - "optional" => { - use syn::{Token, Error}; - let nullable = if input.peek(Token![=]) { - input.parse::()?; - let span = input.span(); - match Ident::parse(input)?.to_string().as_str() { - "nullable" => true, - _ => Err(Error::new(span, "expected 'nullable'"))? - } - } else { - false - }; - out.optional = Optional { - optional: true, - nullable, - } - }, - "flatten" => out.flatten = true, - } -} - -impl_parse! { - Serde(input, out) { - "rename" => out.0.rename = Some(parse_assign_str(input)?), - "skip" => out.0.skip = true, - "flatten" => out.0.flatten = true, - // parse #[serde(default)] to not emit a warning - "default" => { - use syn::Token; - if input.peek(Token![=]) { - parse_assign_str(input)?; - } - }, - "with" => { - parse_assign_str(input)?; - out.0.using_serde_with = true; - }, - } -} - -fn replace_underscore(ty: &mut Type, with: &Type) { - match ty { - Type::Infer(_) => *ty = with.clone(), - Type::Array(TypeArray { elem, .. }) - | Type::Group(TypeGroup { elem, .. }) - | Type::Paren(TypeParen { elem, .. }) - | Type::Ptr(TypePtr { elem, .. }) - | Type::Reference(TypeReference { elem, .. }) - | Type::Slice(TypeSlice { elem, .. }) => { - replace_underscore(elem, with); - } - Type::Tuple(TypeTuple { elems, .. }) => { - for elem in elems { - replace_underscore(elem, with); - } - } - Type::Path(TypePath { path, qself }) => { - if let Some(QSelf { ty, .. }) = qself { - replace_underscore(ty, with); - } - - for segment in &mut path.segments { - match &mut segment.arguments { - PathArguments::None => (), - PathArguments::AngleBracketed(a) => { - replace_underscore_in_angle_bracketed(a, with); - } - PathArguments::Parenthesized(p) => { - for input in &mut p.inputs { - replace_underscore(input, with); - } - if let ReturnType::Type(_, output) = &mut p.output { - replace_underscore(output, with); - } - } - } - } - } - _ => (), - } -} - -fn replace_underscore_in_angle_bracketed(args: &mut AngleBracketedGenericArguments, with: &Type) { - for arg in &mut args.args { - match arg { - GenericArgument::Type(ty) => { - replace_underscore(ty, with); - } - GenericArgument::AssocType(assoc_ty) => { - replace_underscore(&mut assoc_ty.ty, with); - for g in &mut assoc_ty.generics { - replace_underscore_in_angle_bracketed(g, with); - } - } - _ => (), - } - } -} +use syn::{ + AngleBracketedGenericArguments, Attribute, Field, GenericArgument, Ident, PathArguments, QSelf, + Result, ReturnType, Type, TypeArray, TypeGroup, TypeParen, TypePath, TypePtr, TypeReference, + TypeSlice, TypeTuple, +}; + +use super::{parse_assign_from_str, parse_assign_str, Attr, Serde}; +use crate::utils::{parse_attrs, parse_docs}; + +#[derive(Default)] +pub struct FieldAttr { + type_as: Option, + pub type_override: Option, + pub rename: Option, + pub inline: bool, + pub skip: bool, + pub optional: Optional, + pub flatten: bool, + pub docs: String, + + pub using_serde_with: bool, +} + +/// Indicates whether the field is marked with `#[ts(optional)]`. +/// `#[ts(optional)]` turns an `t: Option` into `t?: T`, while +/// `#[ts(optional = nullable)]` turns it into `t?: T | null`. +#[derive(Default)] +pub struct Optional { + pub optional: bool, + pub nullable: bool, +} + +impl FieldAttr { + pub fn from_attrs(attrs: &[Attribute]) -> Result { + let mut result = parse_attrs::(attrs)?; + + if cfg!(feature = "serde-compat") && !result.skip { + let serde_attr = crate::utils::parse_serde_attrs::(attrs); + result = result.merge(serde_attr.0); + } + + result.docs = parse_docs(attrs)?; + + Ok(result) + } + + pub fn type_as(&self, original_type: &Type) -> Type { + if let Some(mut ty) = self.type_as.clone() { + replace_underscore(&mut ty, original_type); + ty + } else { + original_type.clone() + } + } +} + +impl Attr for FieldAttr { + type Item = Field; + + fn merge(self, other: Self) -> Self { + Self { + type_as: self.type_as.or(other.type_as), + type_override: self.type_override.or(other.type_override), + rename: self.rename.or(other.rename), + inline: self.inline || other.inline, + skip: self.skip || other.skip, + optional: Optional { + optional: self.optional.optional || other.optional.optional, + nullable: self.optional.nullable || other.optional.nullable, + }, + flatten: self.flatten || other.flatten, + + using_serde_with: self.using_serde_with || other.using_serde_with, + + // We can't emit TSDoc for a flattened field + // and we cant make this invalid in assert_validity because + // this documentation is totally valid in Rust + docs: if self.flatten || other.flatten { + String::new() + } else { + self.docs + &other.docs + }, + } + } + + fn assert_validity(&self, field: &Self::Item) -> Result<()> { + if cfg!(feature = "serde-compat") + && self.using_serde_with + && !(self.type_as.is_some() || self.type_override.is_some()) + { + syn_err_spanned!( + field; + r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"# + ) + } + + if self.type_override.is_some() { + if self.type_as.is_some() { + syn_err_spanned!(field; "`type` is not compatible with `as`") + } + + if self.inline { + syn_err_spanned!(field; "`type` is not compatible with `inline`") + } + + if self.flatten { + syn_err_spanned!( + field; + "`type` is not compatible with `flatten`" + ); + } + } + + if self.flatten { + if self.type_as.is_some() { + syn_err_spanned!( + field; + "`as` is not compatible with `flatten`" + ); + } + + if self.rename.is_some() { + syn_err_spanned!( + field; + "`rename` is not compatible with `flatten`" + ); + } + + if self.inline { + syn_err_spanned!( + field; + "`inline` is not compatible with `flatten`" + ); + } + + if self.optional.optional { + syn_err_spanned!( + field; + "`optional` is not compatible with `flatten`" + ); + } + } + + if field.ident.is_none() { + if self.flatten { + syn_err_spanned!( + field; + "`flatten` cannot with tuple struct fields" + ); + } + + if self.rename.is_some() { + syn_err_spanned!( + field; + "`flatten` cannot with tuple struct fields" + ); + } + + if self.optional.optional { + syn_err_spanned!( + field; + "`optional` cannot with tuple struct fields" + ); + } + } + + Ok(()) + } +} + +impl_parse! { + FieldAttr(input, out) { + "as" => out.type_as = Some(parse_assign_from_str(input)?), + "type" => out.type_override = Some(parse_assign_str(input)?), + "rename" => out.rename = Some(parse_assign_str(input)?), + "inline" => out.inline = true, + "skip" => out.skip = true, + "optional" => { + use syn::{Token, Error}; + let nullable = if input.peek(Token![=]) { + input.parse::()?; + let span = input.span(); + match Ident::parse(input)?.to_string().as_str() { + "nullable" => true, + _ => Err(Error::new(span, "expected 'nullable'"))? + } + } else { + false + }; + out.optional = Optional { + optional: true, + nullable, + } + }, + "flatten" => out.flatten = true, + } +} + +impl_parse! { + Serde(input, out) { + "rename" => out.0.rename = Some(parse_assign_str(input)?), + "skip" => out.0.skip = true, + "flatten" => out.0.flatten = true, + // parse #[serde(default)] to not emit a warning + "default" => { + use syn::Token; + if input.peek(Token![=]) { + parse_assign_str(input)?; + } + }, + "with" => { + parse_assign_str(input)?; + out.0.using_serde_with = true; + }, + } +} + +fn replace_underscore(ty: &mut Type, with: &Type) { + match ty { + Type::Infer(_) => *ty = with.clone(), + Type::Array(TypeArray { elem, .. }) + | Type::Group(TypeGroup { elem, .. }) + | Type::Paren(TypeParen { elem, .. }) + | Type::Ptr(TypePtr { elem, .. }) + | Type::Reference(TypeReference { elem, .. }) + | Type::Slice(TypeSlice { elem, .. }) => { + replace_underscore(elem, with); + } + Type::Tuple(TypeTuple { elems, .. }) => { + for elem in elems { + replace_underscore(elem, with); + } + } + Type::Path(TypePath { path, qself }) => { + if let Some(QSelf { ty, .. }) = qself { + replace_underscore(ty, with); + } + + for segment in &mut path.segments { + match &mut segment.arguments { + PathArguments::None => (), + PathArguments::AngleBracketed(a) => { + replace_underscore_in_angle_bracketed(a, with); + } + PathArguments::Parenthesized(p) => { + for input in &mut p.inputs { + replace_underscore(input, with); + } + if let ReturnType::Type(_, output) = &mut p.output { + replace_underscore(output, with); + } + } + } + } + } + _ => (), + } +} + +fn replace_underscore_in_angle_bracketed(args: &mut AngleBracketedGenericArguments, with: &Type) { + for arg in &mut args.args { + match arg { + GenericArgument::Type(ty) => { + replace_underscore(ty, with); + } + GenericArgument::AssocType(assoc_ty) => { + replace_underscore(&mut assoc_ty.ty, with); + if let Some(g) = &mut assoc_ty.generics { + replace_underscore_in_angle_bracketed(g, with); + } + } + _ => (), + } + } +} diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index bf2bf660..0b562e50 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -9,7 +9,7 @@ use std::{ sync::Mutex, }; -pub use error::Error; +pub use error::ExportError; use lazy_static::lazy_static; use path::diff_paths; pub(crate) use recursive_export::export_all_into; @@ -29,14 +29,14 @@ mod recursive_export { use std::{any::TypeId, collections::HashSet, path::Path}; use super::export_into; - use crate::{Error, TypeVisitor, TS}; + use crate::{ExportError, TypeVisitor, TS}; /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute within the given /// base directory. /// Additionally, all dependencies of `T` will be exported as well. pub(crate) fn export_all_into( out_dir: impl AsRef, - ) -> Result<(), Error> { + ) -> Result<(), ExportError> { let mut seen = HashSet::new(); export_recursive::(&mut seen, out_dir) } @@ -44,7 +44,7 @@ mod recursive_export { struct Visit<'a> { seen: &'a mut HashSet, out_dir: &'a Path, - error: Option, + error: Option, } impl<'a> TypeVisitor for Visit<'a> { @@ -63,7 +63,7 @@ mod recursive_export { fn export_recursive( seen: &mut HashSet, out_dir: impl AsRef, - ) -> Result<(), Error> { + ) -> Result<(), ExportError> { if !seen.insert(TypeId::of::()) { return Ok(()); } @@ -89,17 +89,19 @@ mod recursive_export { /// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute pub(crate) fn export_into( out_dir: impl AsRef, -) -> Result<(), Error> { +) -> Result<(), ExportError> { let path = T::output_path() .ok_or_else(std::any::type_name::) - .map_err(Error::CannotBeExported)?; + .map_err(ExportError::CannotBeExported)?; let path = out_dir.as_ref().join(path); export_to::(path::absolute(path)?) } /// Export `T` to the file specified by the `path` argument. -pub(crate) fn export_to>(path: P) -> Result<(), Error> { +pub(crate) fn export_to>( + path: P, +) -> Result<(), ExportError> { let path = path.as_ref().to_owned(); let type_name = T::ident(); @@ -113,7 +115,7 @@ pub(crate) fn export_to>(path: P) -> Re let fmt_cfg = ConfigurationBuilder::new().deno().build(); if let Some(formatted) = format_text(path.as_ref(), &buffer, &fmt_cfg) - .map_err(|e| Error::Formatting(e.to_string()))? + .map_err(|e| ExportError::Formatting(e.to_string()))? { buffer = formatted; } @@ -130,7 +132,11 @@ pub(crate) fn export_to>(path: P) -> Re /// Exports the type to a new file if the file hasn't yet been written to. /// Otherwise, finds its place in the already existing file and inserts it. -fn export_and_merge(path: PathBuf, type_name: String, generated_type: String) -> Result<(), Error> { +fn export_and_merge( + path: PathBuf, + type_name: String, + generated_type: String, +) -> Result<(), ExportError> { use std::io::{Read, Write}; let mut lock = EXPORT_PATHS.lock().unwrap(); @@ -253,7 +259,7 @@ fn merge(original_contents: String, new_contents: String) -> String { } /// Returns the generated definition for `T`. -pub(crate) fn export_to_string() -> Result { +pub(crate) fn export_to_string() -> Result { let mut buffer = String::with_capacity(1024); buffer.push_str(NOTE); generate_imports::(&mut buffer, default_out_dir())?; @@ -286,11 +292,11 @@ fn generate_decl(out: &mut String) { fn generate_imports( out: &mut String, out_dir: impl AsRef, -) -> Result<(), Error> { +) -> Result<(), ExportError> { let path = T::output_path() .ok_or_else(std::any::type_name::) .map(|x| out_dir.as_ref().join(x)) - .map_err(Error::CannotBeExported)?; + .map_err(ExportError::CannotBeExported)?; let deps = T::dependencies(); let deduplicated_deps = deps @@ -326,7 +332,7 @@ fn generate_imports( } /// Returns the required import path for importing `import` from the file `from` -fn import_path(from: &Path, import: &Path) -> Result { +fn import_path(from: &Path, import: &Path) -> Result { let rel_path = diff_paths(import, from.parent().unwrap())?; let path = match rel_path.components().next() { Some(Component::Normal(_)) => format!("./{}", rel_path.to_string_lossy()), diff --git a/ts-rs/src/export/error.rs b/ts-rs/src/export/error.rs index e3add3c4..f6068802 100644 --- a/ts-rs/src/export/error.rs +++ b/ts-rs/src/export/error.rs @@ -1,6 +1,6 @@ /// An error which may occur when exporting a type #[derive(thiserror::Error, Debug)] -pub enum Error { +pub enum ExportError { #[error("this type cannot be exported")] CannotBeExported(&'static str), #[cfg(feature = "format")] diff --git a/ts-rs/src/export/path.rs b/ts-rs/src/export/path.rs index 447bf1ab..f43b8c7f 100644 --- a/ts-rs/src/export/path.rs +++ b/ts-rs/src/export/path.rs @@ -1,19 +1,18 @@ use std::path::{Component as C, Path, PathBuf}; -use super::Error; +use super::ExportError as E; const ERROR_MESSAGE: &str = r#"The path provided with `#[ts(export_to = "..")]` is not valid"#; -pub fn absolute>(path: T) -> Result { +pub fn absolute>(path: T) -> Result { let path = std::env::current_dir()?.join(path.as_ref()); - let mut out = Vec::new(); for comp in path.components() { match comp { C::CurDir => (), C::ParentDir => { - out.pop().ok_or(Error::CannotBeExported(ERROR_MESSAGE))?; + out.pop().ok_or(E::CannotBeExported(ERROR_MESSAGE))?; } comp => out.push(comp), } @@ -38,7 +37,7 @@ pub fn absolute>(path: T) -> Result { // // Adapted from rustc's path_relative_from // https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158 -pub(super) fn diff_paths(path: P, base: B) -> Result +pub(super) fn diff_paths(path: P, base: B) -> Result where P: AsRef, B: AsRef, diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 2912fcbd..2f7b19cd 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -130,7 +130,7 @@ use std::{ pub use ts_rs_macros::TS; -pub use crate::export::Error; +pub use crate::export::ExportError; #[cfg(feature = "chrono-impl")] mod chrono; @@ -470,13 +470,13 @@ pub trait TS { /// /// To alter the filename or path of the type within the target directory, /// use `#[ts(export_to = "...")]`. - fn export() -> Result<(), Error> + fn export() -> Result<(), ExportError> where Self: 'static, { let path = Self::default_output_path() .ok_or_else(std::any::type_name::) - .map_err(Error::CannotBeExported)?; + .map_err(ExportError::CannotBeExported)?; export::export_to::(path) } @@ -497,7 +497,7 @@ pub trait TS { /// /// To alter the filenames or paths of the types within the target directory, /// use `#[ts(export_to = "...")]`. - fn export_all() -> Result<(), Error> + fn export_all() -> Result<(), ExportError> where Self: 'static, { @@ -517,7 +517,7 @@ pub trait TS { /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be /// exported automatically whenever `cargo test` is run. /// In that case, there is no need to manually call this function. - fn export_all_to(out_dir: impl AsRef) -> Result<(), Error> + fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> where Self: 'static, { @@ -531,7 +531,7 @@ pub trait TS { /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be /// exported automatically whenever `cargo test` is run. /// In that case, there is no need to manually call this function. - fn export_to_string() -> Result + fn export_to_string() -> Result where Self: 'static, { diff --git a/ts-rs/tests/integration/issue_308.rs b/ts-rs/tests/integration/issue_308.rs index 3a3d915c..41fc5e79 100644 --- a/ts-rs/tests/integration/issue_308.rs +++ b/ts-rs/tests/integration/issue_308.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use ts_rs::{Dependency, Error, TypeVisitor, TS}; +use ts_rs::{Dependency, ExportError, TypeVisitor, TS}; #[rustfmt::skip] trait Malicious { @@ -16,10 +16,10 @@ trait Malicious { fn dependencies() -> Vec { unimplemented!() } fn visit_dependencies(_: &mut impl TypeVisitor) { unimplemented!() } fn visit_generics(_: &mut impl TypeVisitor) { unimplemented!() } - fn export() -> Result<(), Error> { unimplemented!() } - fn export_all() -> Result<(), Error> { unimplemented!() } - fn export_all_to(out_dir: impl AsRef) -> Result<(), Error> { unimplemented!() } - fn export_to_string() -> Result { unimplemented!() } + fn export() -> Result<(), ExportError> { unimplemented!() } + fn export_all() -> Result<(), ExportError> { unimplemented!() } + fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> { unimplemented!() } + fn export_to_string() -> Result { unimplemented!() } fn output_path() -> Option<&'static Path> { unimplemented!() } fn default_output_path() -> Option { unimplemented!() } }