Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise API for manual exports #263

Merged
merged 8 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
- Added `TS::dependency_types()` ([#221](https://github.com/Aleph-Alpha/ts-rs/pull/221))
- Added `TS::generics()` ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
- Added `TS::WithoutGenerics` ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
- `Result`, `Option`, `HashMap` and `Vec` had their implementations of `TS` changed ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
- Removed `TS::transparent()` ([#243](https://github.com/Aleph-Alpha/ts-rs/pull/243))
- Handling of output paths ([#247](https://github.com/Aleph-Alpha/ts-rs/pull/247), [#250](https://github.com/Aleph-Alpha/ts-rs/pull/250), [#256](https://github.com/Aleph-Alpha/ts-rs/pull/256))
- All paths specified using `#[ts(export_to = "...")]` are now relative to `TS_RS_EXPORT_DIR`, which defaults to `./bindings/`
- Replace `TS::export` with `TS::export`, `TS::export_all` and `TS::export_to_all` ([#263](https://github.com/Aleph-Alpha/ts-rs/pull/263))

### Features

Expand All @@ -27,11 +29,14 @@
- Support `#[serde(untagged)]` on individual enum variants ([#226](https://github.com/Aleph-Alpha/ts-rs/pull/226))
- Support for `#[serde(rename_all_fields = "...")]` ([#225](https://github.com/Aleph-Alpha/ts-rs/pull/225))
- Export Rust doc comments/attributes on structs/enums as TSDoc strings ([#187](https://github.com/Aleph-Alpha/ts-rs/pull/187))
- `Result`, `Option`, `HashMap` and `Vec` had their implementations of `TS` changed ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
- Implement `#[ts(...)]` equivalent for `#[serde(tag = "...")]` being used on a struct with named fields ([#244](https://github.com/Aleph-Alpha/ts-rs/pull/244))

### Fixes

- fix `#[ts(skip)]` and `#[serde(skip)]` in variants of adjacently or internally tagged enums ([#231](https://github.com/Aleph-Alpha/ts-rs/pull/231))
- Fix `#[ts(skip)]` and `#[serde(skip)]` in variants of adjacently or internally tagged enums ([#231](https://github.com/Aleph-Alpha/ts-rs/pull/231))
- `rename_all` with `camelCase` produces wrong names if fields were already in camelCase ([#198](https://github.com/Aleph-Alpha/ts-rs/pull/198))
- Improve support for references ([#199](https://github.com/Aleph-Alpha/ts-rs/pull/199))
- Generic type aliases generate correctly ([#233](https://github.com/Aleph-Alpha/ts-rs/pull/233))
- Improve compiler errors ([#257](https://github.com/Aleph-Alpha/ts-rs/pull/257))
- Update dependencies ([#255](https://github.com/Aleph-Alpha/ts-rs/pull/255))
9 changes: 3 additions & 6 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,8 @@ impl DerivedTS {
};

quote! {
fn output_path() -> Option<std::path::PathBuf> {
let path = std::env::var("TS_RS_EXPORT_DIR");
let path = path.as_deref().unwrap_or("./bindings");

Some(std::path::Path::new(path).join(#path))
fn output_path() -> Option<&'static std::path::Path> {
Some(std::path::Path::new(#path))
}
}
};
Expand Down Expand Up @@ -164,7 +161,7 @@ impl DerivedTS {
#[cfg(test)]
#[test]
fn #test_fn() {
#ty::export().expect("could not export type");
#ty::export_all().expect("could not export type");
}
}
}
Expand Down
85 changes: 60 additions & 25 deletions ts-rs/src/export.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::{
any::TypeId,
borrow::Cow,
collections::BTreeMap,
fmt::Write,
fs::File,
path::{Component, Path, PathBuf},
sync::Mutex,
};

pub(crate) use recursive_export::export_type_with_dependencies;
pub(crate) use recursive_export::export_all_into;
use thiserror::Error;

use crate::TS;
Expand All @@ -30,16 +32,27 @@ pub enum ExportError {
}

mod recursive_export {
use std::{any::TypeId, collections::HashSet};
use std::{any::TypeId, collections::HashSet, path::Path};

use super::export_type;
use super::export_into;
use crate::{
typelist::{TypeList, TypeVisitor},
ExportError, 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<T: TS + ?Sized + 'static>(
out_dir: impl AsRef<Path>,
) -> Result<(), ExportError> {
let mut seen = HashSet::new();
export_recursive::<T>(&mut seen, out_dir)
}

struct Visit<'a> {
seen: &'a mut HashSet<TypeId>,
out_dir: &'a Path,
error: Option<ExportError>,
}

Expand All @@ -51,29 +64,27 @@ mod recursive_export {
return;
}

self.error = export_recursive::<T>(self.seen).err();
self.error = export_recursive::<T>(self.seen, self.out_dir).err();
}
}

/// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute.
/// Additionally, all dependencies of `T` will be exported as well.
pub(crate) fn export_type_with_dependencies<T: TS + ?Sized + 'static>(
) -> Result<(), ExportError> {
let mut seen = HashSet::new();
export_recursive::<T>(&mut seen)
}

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

export_type::<T>()?;
export_into::<T>(out_dir)?;

let mut visitor = Visit { seen, error: None };
let mut visitor = Visit {
seen,
out_dir,
error: None,
};
T::dependency_types().for_each(&mut visitor);

if let Some(e) = visitor.error {
Expand All @@ -85,15 +96,19 @@ mod recursive_export {
}

/// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute
pub(crate) fn export_type<T: TS + ?Sized + 'static>() -> Result<(), ExportError> {
pub(crate) fn export_into<T: TS + ?Sized + 'static>(
out_dir: impl AsRef<Path>,
) -> Result<(), ExportError> {
let path = T::output_path()
.ok_or_else(std::any::type_name::<T>)
.map_err(ExportError::CannotBeExported)?;
export_type_to::<T, _>(path::absolute(path)?)
let path = out_dir.as_ref().join(path);

export_to::<T, _>(path::absolute(path)?)
}

/// Export `T` to the file specified by the `path` argument.
pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
pub(crate) fn export_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
path: P,
) -> Result<(), ExportError> {
// Lock to make sure only one file will be written at a time.
Expand All @@ -102,7 +117,7 @@ pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
static FILE_LOCK: Mutex<()> = Mutex::new(());

#[allow(unused_mut)]
let mut buffer = export_type_to_string::<T>()?;
let mut buffer = export_to_string::<T>()?;

// format output
#[cfg(feature = "format")]
Expand All @@ -121,20 +136,35 @@ pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
std::fs::create_dir_all(parent)?;
}
let lock = FILE_LOCK.lock().unwrap();
std::fs::write(path.as_ref(), buffer)?;
{
// 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()?;
}

drop(lock);
Ok(())
}

/// Returns the generated defintion for `T`.
pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
/// Returns the generated definition for `T`.
pub(crate) fn export_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
let mut buffer = String::with_capacity(1024);
buffer.push_str(NOTE);
generate_imports::<T::WithoutGenerics>(&mut buffer)?;
generate_imports::<T::WithoutGenerics>(&mut buffer, default_out_dir())?;
generate_decl::<T>(&mut buffer);
Ok(buffer)
}

pub(crate) fn default_out_dir() -> Cow<'static, Path> {
match std::env::var("TS_RS_EXPORT_DIR") {
Err(..) => Cow::Borrowed(Path::new("./bindings")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I didn't know Err(..) was valid syntax to ignore enum data, I though it had to be Err(_)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a character more, but I kinda like how it looks πŸ˜†

Ok(dir) => Cow::Owned(PathBuf::from(dir)),
}
}

/// Push the declaration of `T`
fn generate_decl<T: TS + ?Sized>(out: &mut String) {
// Type Docs
Expand All @@ -148,11 +178,15 @@ fn generate_decl<T: TS + ?Sized>(out: &mut String) {
out.push_str(&T::decl());
}

/// Push an import statement for all dependencies of `T`
fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), ExportError> {
/// Push an import statement for all dependencies of `T`.
fn generate_imports<T: TS + ?Sized + 'static>(
out: &mut String,
out_dir: impl AsRef<Path>,
) -> Result<(), ExportError> {
let path = T::output_path()
.ok_or_else(std::any::type_name::<T>)
.map_err(ExportError::CannotBeExported)?;
let path = out_dir.as_ref().join(path);

let deps = T::dependencies();
let deduplicated_deps = deps
Expand All @@ -162,7 +196,8 @@ fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), Ex
.collect::<BTreeMap<_, _>>();

for (_, dep) in deduplicated_deps {
let rel_path = import_path(&path, Path::new(&dep.exported_to));
let dep_path = out_dir.as_ref().join(dep.output_path);
let rel_path = import_path(&path, &dep_path);
writeln!(
out,
"import type {{ {} }} from {:?};",
Expand Down
Loading
Loading