diff --git a/CHANGELOG.md b/CHANGELOG.md index 933fc3b4..9c7b38ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Features - The `#[ts(rename)]` attribute on structs, enums and variants now accepts any expression. This makes it possible to, for example, rename a struct to the name of a module it is contained in using `#[ts(rename = module_path!().rsplit_once("::").unwrap().1)]` +- The `#[ts(export_to)]` attribute on structs and enums now accepts any expression. - Added `#[ts(optional_fields)]` and `#[ts(optional_fields = nullable)]` attribute to structs, this attribute is equivalent to using the corresponding `#[ts(optional)]` or `#[ts(optional = nullable)]` on every field of the struct. ([#366](https://github.com/Aleph-Alpha/ts-rs/pull/366)) ### Fixes diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index efd542df..11e83d3b 100644 --- a/macros/src/attr/enum.rs +++ b/macros/src/attr/enum.rs @@ -16,7 +16,7 @@ pub struct EnumAttr { pub rename_all: Option, pub rename_all_fields: Option, pub rename: Option, - pub export_to: Option, + pub export_to: Option, pub export: bool, pub docs: String, pub concrete: HashMap, @@ -212,7 +212,7 @@ impl_parse! { "rename" => out.rename = Some(parse_assign_expr(input)?), "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), "rename_all_fields" => out.rename_all_fields = Some(parse_assign_inflection(input)?), - "export_to" => out.export_to = Some(parse_assign_str(input)?), + "export_to" => out.export_to = Some(parse_assign_expr(input)?), "export" => out.export = true, "tag" => out.tag = Some(parse_assign_str(input)?), "content" => out.content = Some(parse_assign_str(input)?), diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index bbf231d0..ffcb52b4 100644 --- a/macros/src/attr/struct.rs +++ b/macros/src/attr/struct.rs @@ -18,7 +18,7 @@ pub struct StructAttr { pub type_override: Option, pub rename_all: Option, pub rename: Option, - pub export_to: Option, + pub export_to: Option, pub export: bool, pub tag: Option, pub docs: String, @@ -163,7 +163,7 @@ impl_parse! { "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), "tag" => out.tag = Some(parse_assign_str(input)?), "export" => out.export = true, - "export_to" => out.export_to = Some(parse_assign_str(input)?), + "export_to" => out.export_to = Some(parse_assign_expr(input)?), "concrete" => out.concrete = parse_concrete(input)?, "bound" => out.bound = Some(parse_bound(input)?), "optional_fields" => out.optional_fields = parse_optional(input)?, diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 1862caf7..8f4eecc1 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -30,7 +30,7 @@ struct DerivedTS { bound: Option>, export: bool, - export_to: Option, + export_to: Option, } impl DerivedTS { @@ -42,14 +42,18 @@ impl DerivedTS { let output_path_fn = { let ts_name = &self.ts_name; // expression of type `String` containing the file path - let path_string = match self.export_to.as_deref() { - Some(dirname) if dirname.ends_with('/') => { - quote![format!("{}{}.ts", #dirname, #ts_name)] - } - Some(filename) => quote![#filename.to_owned()], - None => { - quote![format!("{}.ts", #ts_name)] - } + let path_string = match &self.export_to { + Some(dir_or_file) => quote![{ + let dir_or_file = format!("{}", #dir_or_file); + if dir_or_file.ends_with('/') { + // export into directory + format!("{dir_or_file}{}.ts", #ts_name) + } else { + // export into provided file + format!("{dir_or_file}") + } + }], + None => quote![format!("{}.ts", #ts_name)], }; quote! { diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 49e65fb5..9cc9303f 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -203,7 +203,8 @@ mod tokio; /// Specifies where the type should be exported to. Defaults to `.ts`. /// The path given to the `export_to` attribute is relative to the `TS_RS_EXPORT_DIR` environment variable, /// or, if `TS_RS_EXPORT_DIR` is not set, to `./bindings` -/// If the provided path ends in a trailing `/`, it is interpreted as a directory. +/// If the provided path ends in a trailing `/`, it is interpreted as a directory. +/// This attribute also accepts arbitrary expressions. /// Note that you need to add the `export` attribute as well, in order to generate a test which exports the type. ///

/// diff --git a/ts-rs/tests/integration/export_to.rs b/ts-rs/tests/integration/export_to.rs new file mode 100644 index 00000000..75fd48b8 --- /dev/null +++ b/ts-rs/tests/integration/export_to.rs @@ -0,0 +1,100 @@ +use std::path::Path; +use ts_rs::TS; + +#[derive(TS)] +#[ts(export, export_to = "export_to/with_str_to_file.ts")] +struct WithStrToFile; + +#[derive(TS)] +#[ts(export, export_to = "export_to/")] +struct WithStrToDir; + +// -- + +#[derive(TS)] +#[ts(export, export_to = &"export_to/with_str_ref_to_file.ts")] +struct WithStrRefToFile; + +#[derive(TS)] +#[ts(export, export_to = &"export_to/")] +struct WithStrRefToDir; + +// -- + +#[derive(TS)] +#[ts(export, export_to = format!("export_to/with_string_to_file.ts"))] +struct WithStringToFile; + +#[derive(TS)] +#[ts(export, export_to = format!("export_to/"))] +struct WithStringToDir; + +// -- + +#[derive(TS)] +#[ts(export, export_to = &format!("export_to/with_string_ref_to_file.ts"))] +struct WithStringRefToFile; + +#[derive(TS)] +#[ts(export, export_to = &format!("export_to/"))] +struct WithStringRefToDir; + +// -- + +#[derive(TS)] +#[ts(export, export_to = { + let dir = WithStrToFile::default_output_path().unwrap(); + let dir = dir.parent().unwrap(); + let file = dir.join("to_absolute_file_path.ts"); + let file = std::path::absolute(file).unwrap(); + file.display().to_string() +})] +struct ToAbsoluteFilePath(WithStrToDir, WithStrToFile); + +#[derive(TS)] +#[ts(export, export_to = { + let dir = WithStrToFile::default_output_path().unwrap(); + let dir = dir.parent().unwrap(); + let dir = std::path::absolute(dir).unwrap(); + let dir = dir.display(); + format!("{dir}/") +})] +struct ToAbsoluteDirPath(WithStrToDir, WithStrToFile, ToAbsoluteFilePath); + +// -- + +#[test] +#[cfg(test)] +fn check_export_complete() { + export_bindings_withstrtofile(); + export_bindings_withstrtodir(); + export_bindings_withstrreftofile(); + export_bindings_withstrreftodir(); + export_bindings_withstringtofile(); + export_bindings_withstringtodir(); + export_bindings_withstringreftofile(); + export_bindings_withstringreftodir(); + export_bindings_toabsolutefilepath(); + export_bindings_toabsolutedirpath(); + + let files = [ + "with_str_to_file.ts", + "WithStrToDir.ts", + "with_str_ref_to_file.ts", + "WithStrRefToDir.ts", + "with_string_to_file.ts", + "WithStringToDir.ts", + "with_string_ref_to_file.ts", + "WithStringRefToDir.ts", + "to_absolute_file_path.ts", + "ToAbsoluteDirPath.ts", + ]; + + let dir = std::env::var("TS_RS_EXPORT_DIR").unwrap_or_else(|_| "./bindings".to_owned()); + let dir = Path::new(&dir).join("export_to"); + + files + .iter() + .map(|file| dir.join(file)) + .for_each(|file| assert!(file.is_file(), "{file:?}")); +} diff --git a/ts-rs/tests/integration/main.rs b/ts-rs/tests/integration/main.rs index c4733953..452e13bc 100644 --- a/ts-rs/tests/integration/main.rs +++ b/ts-rs/tests/integration/main.rs @@ -12,6 +12,7 @@ mod enum_flattening_nested; mod enum_struct_rename_all; mod enum_variant_annotation; mod export_manually; +mod export_to; mod field_rename; mod flatten; mod generic_fields;