├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── e2e ├── README.md ├── dependencies │ ├── consumer │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── dependency1 │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs └── workspace │ ├── Cargo.lock │ ├── Cargo.toml │ ├── crate1 │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ ├── crate2 │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ ├── parent │ ├── Cargo.toml │ └── src │ │ └── main.rs │ └── renamed │ ├── Cargo.toml │ └── src │ └── main.rs ├── example ├── Cargo.toml └── src │ └── lib.rs ├── logo.png ├── macros ├── Cargo.toml └── src │ ├── attr │ ├── enum.rs │ ├── field.rs │ ├── mod.rs │ ├── struct.rs │ └── variant.rs │ ├── deps.rs │ ├── lib.rs │ ├── optional.rs │ ├── types │ ├── enum.rs │ ├── mod.rs │ ├── named.rs │ ├── newtype.rs │ ├── tuple.rs │ ├── type_as.rs │ ├── type_override.rs │ └── unit.rs │ └── utils.rs ├── rustfmt.toml └── ts-rs ├── Cargo.toml ├── src ├── chrono.rs ├── export.rs ├── export │ ├── error.rs │ └── path.rs ├── lib.rs ├── serde_json.rs └── tokio.rs └── tests └── integration ├── arrays.rs ├── bound.rs ├── bson.rs ├── chrono.rs ├── complex_flattened_type.rs ├── concrete_generic.rs ├── docs.rs ├── enum_flattening.rs ├── enum_flattening_nested.rs ├── enum_struct_rename_all.rs ├── enum_variant_annotation.rs ├── export_manually.rs ├── export_to.rs ├── field_rename.rs ├── flatten.rs ├── generic_fields.rs ├── generic_without_import.rs ├── generics.rs ├── generics_flatten.rs ├── hashmap.rs ├── hashset.rs ├── impl_primitive.rs ├── imports.rs ├── indexmap.rs ├── infer_as.rs ├── issue_168.rs ├── issue_232.rs ├── issue_308.rs ├── issue_317.rs ├── issue_338.rs ├── issue_397.rs ├── issue_415.rs ├── issue_70.rs ├── issue_80.rs ├── leading_colon.rs ├── lifetimes.rs ├── list.rs ├── main.rs ├── merge_same_file_imports.rs ├── nested.rs ├── optional_field.rs ├── path_bug.rs ├── ranges.rs ├── raw_idents.rs ├── recursion_limit.rs ├── references.rs ├── same_file_export.rs ├── self_referential.rs ├── semver.rs ├── serde_json.rs ├── serde_skip_serializing.rs ├── serde_skip_with_default.rs ├── serde_with.rs ├── simple.rs ├── skip.rs ├── slices.rs ├── struct_rename.rs ├── struct_tag.rs ├── tokio.rs ├── top_level_type_as.rs ├── top_level_type_override.rs ├── tuple.rs ├── type_as.rs ├── type_override.rs ├── union.rs ├── union_named_serde_skip.rs ├── union_rename.rs ├── union_serde.rs ├── union_unnamed_serde_skip.rs ├── union_with_data.rs ├── union_with_internal_tag.rs ├── unit.rs └── unsized.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'bug: ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Create the following type '....' 17 | 3. Derive/implement `TS` 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version** 27 | What version of the library are you using? 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Feature request: ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | What is this PR attempting to achieve? Is it a bug fix? Is it related to an issue? 4 | Closes # 5 | 6 | ## Changes 7 | 8 | How did you go about solving the problem? 9 | 10 | ## Checklist 11 | 12 | - [ ] I have followed the steps listed in the [Contributing guide](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md). 13 | - [ ] If necessary, I have added documentation related to the changes made. 14 | - [ ] I have added or updated the tests related to the changes made. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | /.idea 3 | *.ts 4 | tsconfig.json 5 | 6 | /ts-rs/tests-out 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | If you are unsure what to work on or want to discuss your idea, feel free to open an issue. 3 | 4 | ### Documentation 5 | After implementing a new feature, please document it in the doc comment on `TS` in `ts_rs/lib.rs`. 6 | `README.md` is generated from the module doc comment in `ts_rs/lib.rs`. If you added/updated documentation there, go to the `ts-rs/` directory and run one of the following commands: 7 | 8 | On Windows: 9 | `cargo readme -o ..\README.md` 10 | 11 | On other systems: 12 | `cargo readme > ../README.md` 13 | 14 | You can install `cargo readme` by running `cargo install cargo-readme`. 15 | 16 | 17 | ### Tests 18 | Please remember to write tests - If you are fixing a bug, write a test first to reproduce it. 19 | 20 | ### Building 21 | There is nothing special going on here - just run `cargo build`. 22 | To run the test suite, just run `cargo test` in the root directory. 23 | 24 | ### Formatting 25 | To ensure proper formatting, please make sure you have the nigthly toolchain installed. 26 | After that, in the project's root directory, create a file called `.git/hooks/pre-commit` without a file extension and add the following two lines: 27 | ```sh 28 | #!/bin/sh 29 | cargo +nightly fmt 30 | ``` 31 | 32 | This will make sure your files are formatted before your commit is sent, so you don't have to manually run `cargo +nightly fmt` 33 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["macros", "ts-rs", "example"] 3 | resolver = "2" 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aleph Alpha GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-rs 2 | 3 |

4 | logo 5 |
6 | ts-rs 7 |

8 |

9 | Generate typescript type declarations from rust types 10 |

11 | 12 |
13 | 14 | actions status 15 | 16 | Crates.io version 18 | 19 | 20 | docs.rs docs 22 | 23 | 24 | Download 26 | 27 |
28 | 29 | ### Why? 30 | When building a web application in rust, data structures have to be shared between backend and frontend. 31 | Using this library, you can easily generate TypeScript bindings to your rust structs & enums so that you can keep your 32 | types in one place. 33 | 34 | ts-rs might also come in handy when working with webassembly. 35 | 36 | ### How? 37 | ts-rs exposes a single trait, `TS`. Using a derive macro, you can implement this interface for your types. 38 | Then, you can use this trait to obtain the TypeScript bindings. 39 | We recommend doing this in your tests. 40 | [See the example](https://github.com/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs) and [the docs](https://docs.rs/ts-rs/latest/ts_rs/). 41 | 42 | ### Get started 43 | ```toml 44 | [dependencies] 45 | ts-rs = "10.1" 46 | ``` 47 | 48 | ```rust 49 | use ts_rs::TS; 50 | 51 | #[derive(TS)] 52 | #[ts(export)] 53 | struct User { 54 | user_id: i32, 55 | first_name: String, 56 | last_name: String, 57 | } 58 | ``` 59 | 60 | When running `cargo test` or `cargo test export_bindings`, the TypeScript bindings will be exported to the file `bindings/User.ts` 61 | and will contain the following code: 62 | 63 | ```ts 64 | export type User = { user_id: number, first_name: string, last_name: string, }; 65 | ``` 66 | 67 | ### Features 68 | - generate type declarations from rust structs 69 | - generate union declarations from rust enums 70 | - inline types 71 | - flatten structs/types 72 | - generate necessary imports when exporting to multiple files 73 | - serde compatibility 74 | - generic types 75 | - support for ESM imports 76 | 77 | ### cargo features 78 | | **Feature** | **Description** | 79 | |:-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 80 | | serde-compat | **Enabled by default**
See the *"serde compatibility"* section below for more information. | 81 | | format | Enables formatting of the generated TypeScript bindings.
Currently, this unfortunately adds quite a few dependencies. | 82 | | no-serde-warnings | By default, warnings are printed during build if unsupported serde attributes are encountered.
Enabling this feature silences these warnings. | 83 | | import-esm | When enabled,`import` statements in the generated file will have the `.js` extension in the end of the path to conform to the ES Modules spec.
Example: `import { MyStruct } from "./my_struct.js"` | 84 | | serde-json-impl | Implement `TS` for types from *serde_json* | 85 | | chrono-impl | Implement `TS` for types from *chrono* | 86 | | bigdecimal-impl | Implement `TS` for types from *bigdecimal* | 87 | | url-impl | Implement `TS` for types from *url* | 88 | | uuid-impl | Implement `TS` for types from *uuid* | 89 | | bson-uuid-impl | Implement `TS` for *bson::oid::ObjectId* and *bson::uuid* | 90 | | bytes-impl | Implement `TS` for types from *bytes* | 91 | | indexmap-impl | Implement `TS` for types from *indexmap* | 92 | | ordered-float-impl | Implement `TS` for types from *ordered_float* | 93 | | heapless-impl | Implement `TS` for types from *heapless* | 94 | | semver-impl | Implement `TS` for types from *semver* | 95 | | smol_str-impl | Implement `TS` for types from *smol_str* | 96 | | tokio-impl | Implement `TS` for types from *tokio* | 97 | 98 |
99 | 100 | If there's a type you're dealing with which doesn't implement `TS`, use either 101 | `#[ts(as = "..")]` or `#[ts(type = "..")]`, or open a PR. 102 | 103 | ### `serde` compatability 104 | With the `serde-compat` feature (enabled by default), serde attributes can be parsed for enums and structs. 105 | Supported serde attributes: 106 | - `rename` 107 | - `rename-all` 108 | - `rename-all-fields` 109 | - `tag` 110 | - `content` 111 | - `untagged` 112 | - `skip` 113 | - `skip_serializing` 114 | - `skip_serializing_if` 115 | - `flatten` 116 | - `default` 117 | 118 | Note: `skip_serializing` and `skip_serializing_if` only have an effect when used together with 119 | `#[serde(default)]`. 120 | 121 | Note: `skip_deserializing` is ignored. If you wish to exclude a field 122 | from the generated type, but cannot use `#[serde(skip)]`, use `#[ts(skip)]` instead. 123 | 124 | When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled. 125 | 126 | ### Contributing 127 | Contributions are always welcome! 128 | Feel free to open an issue, discuss using GitHub discussions or open a PR. 129 | [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md) 130 | 131 | ### MSRV 132 | The Minimum Supported Rust Version for this crate is 1.78.0 133 | 134 | License: MIT 135 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # e2e tests 2 | ### [dependencies](./dependencies) 3 | A user creates the crate `consumer`, which depends on some crate `dependency1`, which possibly lives on [crates.io](https://crates.io). 4 | If a struct in `consumer` contains a type from `dependency1`, it should be exported as well. 5 | 6 | ### [workspace](./workspace) 7 | A user creates a workspace, containing `crate1`, `crate2`, and `parent`. 8 | `crate1` and `crate2` are independent, but `parent` depends on both `crate1` and `crate2`. 9 | -------------------------------------------------------------------------------- /e2e/dependencies/consumer/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | 11 | [[package]] 12 | name = "consumer" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "dependency1", 16 | "ts-rs", 17 | ] 18 | 19 | [[package]] 20 | name = "dependency1" 21 | version = "0.1.0" 22 | dependencies = [ 23 | "ts-rs", 24 | ] 25 | 26 | [[package]] 27 | name = "proc-macro2" 28 | version = "1.0.78" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 31 | dependencies = [ 32 | "unicode-ident", 33 | ] 34 | 35 | [[package]] 36 | name = "quote" 37 | version = "1.0.35" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 40 | dependencies = [ 41 | "proc-macro2", 42 | ] 43 | 44 | [[package]] 45 | name = "syn" 46 | version = "2.0.48" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 49 | dependencies = [ 50 | "proc-macro2", 51 | "quote", 52 | "unicode-ident", 53 | ] 54 | 55 | [[package]] 56 | name = "termcolor" 57 | version = "1.4.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 60 | dependencies = [ 61 | "winapi-util", 62 | ] 63 | 64 | [[package]] 65 | name = "thiserror" 66 | version = "1.0.56" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 69 | dependencies = [ 70 | "thiserror-impl", 71 | ] 72 | 73 | [[package]] 74 | name = "thiserror-impl" 75 | version = "1.0.56" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 78 | dependencies = [ 79 | "proc-macro2", 80 | "quote", 81 | "syn", 82 | ] 83 | 84 | [[package]] 85 | name = "ts-rs" 86 | version = "7.1.1" 87 | dependencies = [ 88 | "thiserror", 89 | "ts-rs-macros", 90 | ] 91 | 92 | [[package]] 93 | name = "ts-rs-macros" 94 | version = "7.1.1" 95 | dependencies = [ 96 | "Inflector", 97 | "proc-macro2", 98 | "quote", 99 | "syn", 100 | "termcolor", 101 | ] 102 | 103 | [[package]] 104 | name = "unicode-ident" 105 | version = "1.0.12" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 108 | 109 | [[package]] 110 | name = "winapi" 111 | version = "0.3.9" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 114 | dependencies = [ 115 | "winapi-i686-pc-windows-gnu", 116 | "winapi-x86_64-pc-windows-gnu", 117 | ] 118 | 119 | [[package]] 120 | name = "winapi-i686-pc-windows-gnu" 121 | version = "0.4.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 124 | 125 | [[package]] 126 | name = "winapi-util" 127 | version = "0.1.6" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 130 | dependencies = [ 131 | "winapi", 132 | ] 133 | 134 | [[package]] 135 | name = "winapi-x86_64-pc-windows-gnu" 136 | version = "0.4.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 139 | -------------------------------------------------------------------------------- /e2e/dependencies/consumer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "consumer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [workspace] 7 | 8 | [dependencies] 9 | ts-rs = { path = "../../../ts-rs" } 10 | dependency1 = { path = "../dependency1" } -------------------------------------------------------------------------------- /e2e/dependencies/consumer/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use dependency1::*; 4 | use ts_rs::TS; 5 | 6 | #[derive(TS)] 7 | #[ts(export)] 8 | struct ConsumerType { 9 | pub ty1: LibraryType1, 10 | pub ty2_1: LibraryType2, 11 | pub ty2_2: LibraryType2<&'static Self>, 12 | pub ty2_3: LibraryType2>>, 13 | pub ty2_4: LibraryType2>, 14 | pub ty2_5: LibraryType2, 15 | } 16 | 17 | #[derive(TS)] 18 | #[ts(export)] 19 | struct T0; 20 | 21 | #[derive(TS)] 22 | #[ts(export)] 23 | struct T1 { 24 | t0: Option, 25 | } 26 | 27 | fn main() {} 28 | -------------------------------------------------------------------------------- /e2e/dependencies/dependency1/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | 11 | [[package]] 12 | name = "dependency1" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "ts-rs", 16 | ] 17 | 18 | [[package]] 19 | name = "proc-macro2" 20 | version = "1.0.78" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 23 | dependencies = [ 24 | "unicode-ident", 25 | ] 26 | 27 | [[package]] 28 | name = "quote" 29 | version = "1.0.35" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 32 | dependencies = [ 33 | "proc-macro2", 34 | ] 35 | 36 | [[package]] 37 | name = "syn" 38 | version = "2.0.48" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 41 | dependencies = [ 42 | "proc-macro2", 43 | "quote", 44 | "unicode-ident", 45 | ] 46 | 47 | [[package]] 48 | name = "termcolor" 49 | version = "1.4.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 52 | dependencies = [ 53 | "winapi-util", 54 | ] 55 | 56 | [[package]] 57 | name = "thiserror" 58 | version = "1.0.56" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 61 | dependencies = [ 62 | "thiserror-impl", 63 | ] 64 | 65 | [[package]] 66 | name = "thiserror-impl" 67 | version = "1.0.56" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 70 | dependencies = [ 71 | "proc-macro2", 72 | "quote", 73 | "syn", 74 | ] 75 | 76 | [[package]] 77 | name = "ts-rs" 78 | version = "7.1.1" 79 | dependencies = [ 80 | "thiserror", 81 | "ts-rs-macros", 82 | ] 83 | 84 | [[package]] 85 | name = "ts-rs-macros" 86 | version = "7.1.1" 87 | dependencies = [ 88 | "Inflector", 89 | "proc-macro2", 90 | "quote", 91 | "syn", 92 | "termcolor", 93 | ] 94 | 95 | [[package]] 96 | name = "unicode-ident" 97 | version = "1.0.12" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 100 | 101 | [[package]] 102 | name = "winapi" 103 | version = "0.3.9" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 106 | dependencies = [ 107 | "winapi-i686-pc-windows-gnu", 108 | "winapi-x86_64-pc-windows-gnu", 109 | ] 110 | 111 | [[package]] 112 | name = "winapi-i686-pc-windows-gnu" 113 | version = "0.4.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 116 | 117 | [[package]] 118 | name = "winapi-util" 119 | version = "0.1.6" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 122 | dependencies = [ 123 | "winapi", 124 | ] 125 | 126 | [[package]] 127 | name = "winapi-x86_64-pc-windows-gnu" 128 | version = "0.4.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 131 | -------------------------------------------------------------------------------- /e2e/dependencies/dependency1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dependency1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [workspace] 7 | 8 | [dependencies] 9 | ts-rs = { path = "../../../ts-rs" } -------------------------------------------------------------------------------- /e2e/dependencies/dependency1/src/lib.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | #[derive(TS)] 4 | pub struct LibraryType1 { 5 | pub a: i32, 6 | } 7 | 8 | #[derive(TS)] 9 | pub struct LibraryType2 { 10 | pub t: T, 11 | } 12 | 13 | #[derive(TS)] 14 | pub struct LibraryType3; 15 | -------------------------------------------------------------------------------- /e2e/workspace/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | 11 | [[package]] 12 | name = "crate1" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "ts-rs", 16 | ] 17 | 18 | [[package]] 19 | name = "crate2" 20 | version = "0.1.0" 21 | dependencies = [ 22 | "ts-rs", 23 | ] 24 | 25 | [[package]] 26 | name = "parent" 27 | version = "0.1.0" 28 | dependencies = [ 29 | "crate1", 30 | "crate2", 31 | "ts-rs", 32 | ] 33 | 34 | [[package]] 35 | name = "proc-macro2" 36 | version = "1.0.78" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 39 | dependencies = [ 40 | "unicode-ident", 41 | ] 42 | 43 | [[package]] 44 | name = "quote" 45 | version = "1.0.35" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 48 | dependencies = [ 49 | "proc-macro2", 50 | ] 51 | 52 | [[package]] 53 | name = "syn" 54 | version = "2.0.48" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 57 | dependencies = [ 58 | "proc-macro2", 59 | "quote", 60 | "unicode-ident", 61 | ] 62 | 63 | [[package]] 64 | name = "termcolor" 65 | version = "1.4.1" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 68 | dependencies = [ 69 | "winapi-util", 70 | ] 71 | 72 | [[package]] 73 | name = "thiserror" 74 | version = "1.0.56" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 77 | dependencies = [ 78 | "thiserror-impl", 79 | ] 80 | 81 | [[package]] 82 | name = "thiserror-impl" 83 | version = "1.0.56" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 86 | dependencies = [ 87 | "proc-macro2", 88 | "quote", 89 | "syn", 90 | ] 91 | 92 | [[package]] 93 | name = "ts-rs" 94 | version = "7.1.1" 95 | dependencies = [ 96 | "thiserror", 97 | "ts-rs-macros", 98 | ] 99 | 100 | [[package]] 101 | name = "ts-rs-macros" 102 | version = "7.1.1" 103 | dependencies = [ 104 | "Inflector", 105 | "proc-macro2", 106 | "quote", 107 | "syn", 108 | "termcolor", 109 | ] 110 | 111 | [[package]] 112 | name = "unicode-ident" 113 | version = "1.0.12" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 116 | 117 | [[package]] 118 | name = "winapi" 119 | version = "0.3.9" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 122 | dependencies = [ 123 | "winapi-i686-pc-windows-gnu", 124 | "winapi-x86_64-pc-windows-gnu", 125 | ] 126 | 127 | [[package]] 128 | name = "winapi-i686-pc-windows-gnu" 129 | version = "0.4.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 132 | 133 | [[package]] 134 | name = "winapi-util" 135 | version = "0.1.6" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 138 | dependencies = [ 139 | "winapi", 140 | ] 141 | 142 | [[package]] 143 | name = "winapi-x86_64-pc-windows-gnu" 144 | version = "0.4.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 147 | -------------------------------------------------------------------------------- /e2e/workspace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crate1", "crate2", "parent", "renamed"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /e2e/workspace/crate1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crate1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ts-rs = { path = "../../../ts-rs" } -------------------------------------------------------------------------------- /e2e/workspace/crate1/src/lib.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | #[derive(TS)] 4 | #[ts(export_to = "crate1/")] 5 | pub struct Crate1 { 6 | pub x: [[[i32; 128]; 128]; 128], 7 | } 8 | -------------------------------------------------------------------------------- /e2e/workspace/crate2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crate2" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ts-rs = { path = "../../../ts-rs" } 8 | -------------------------------------------------------------------------------- /e2e/workspace/crate2/src/lib.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | #[derive(TS)] 4 | pub struct Crate2 { 5 | pub x: [[[i32; 128]; 128]; 128], 6 | } 7 | -------------------------------------------------------------------------------- /e2e/workspace/parent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parent" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ts-rs = { path = "../../../ts-rs" } 8 | crate1 = { path = "../crate1" } 9 | crate2 = { path = "../crate2" } 10 | -------------------------------------------------------------------------------- /e2e/workspace/parent/src/main.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | use crate1::Crate1; 3 | use crate2::Crate2; 4 | 5 | fn main() { 6 | println!("Hello, world!"); 7 | } 8 | 9 | #[derive(TS)] 10 | #[ts(export)] 11 | pub struct Parent { 12 | pub crate1: Crate1, 13 | pub crate2: Crate2, 14 | } 15 | #[derive(TS)] 16 | #[ts(export)] 17 | pub struct Parent2 { 18 | pub crate1: Crate1, 19 | pub crate2: Crate2, 20 | } 21 | #[derive(TS)] 22 | #[ts(export)] 23 | pub struct Parent3 { 24 | pub crate1: Crate1, 25 | pub crate2: Crate2, 26 | } 27 | #[derive(TS)] 28 | #[ts(export)] 29 | pub struct Parent4 { 30 | pub crate1: Crate1, 31 | pub crate2: Crate2, 32 | } 33 | #[derive(TS)] 34 | #[ts(export)] 35 | pub struct Parent5 { 36 | pub crate1: Crate1, 37 | pub crate2: Crate2, 38 | } 39 | #[derive(TS)] 40 | #[ts(export)] 41 | pub struct Parent6 { 42 | pub crate1: Crate1, 43 | pub crate2: Crate2, 44 | } 45 | #[derive(TS)] 46 | #[ts(export)] 47 | pub struct Parent7 { 48 | pub crate1: Crate1, 49 | pub crate2: Crate2, 50 | } 51 | #[derive(TS)] 52 | #[ts(export)] 53 | pub struct Parent8 { 54 | pub crate1: Crate1, 55 | pub crate2: Crate2, 56 | } 57 | #[derive(TS)] 58 | #[ts(export)] 59 | pub struct Parent9 { 60 | pub crate1: Crate1, 61 | pub crate2: Crate2, 62 | } 63 | #[derive(TS)] 64 | #[ts(export)] 65 | pub struct Parent10 { 66 | pub crate1: Crate1, 67 | pub crate2: Crate2, 68 | } 69 | #[derive(TS)] 70 | #[ts(export)] 71 | pub struct Parent11 { 72 | pub crate1: Crate1, 73 | pub crate2: Crate2, 74 | } 75 | #[derive(TS)] 76 | #[ts(export)] 77 | pub struct Parent12 { 78 | pub crate1: Crate1, 79 | pub crate2: Crate2, 80 | } 81 | #[derive(TS)] 82 | #[ts(export)] 83 | pub struct Parent13 { 84 | pub crate1: Crate1, 85 | pub crate2: Crate2, 86 | } 87 | #[derive(TS)] 88 | #[ts(export)] 89 | pub struct Parent14 { 90 | pub crate1: Crate1, 91 | pub crate2: Crate2, 92 | } 93 | #[derive(TS)] 94 | #[ts(export)] 95 | pub struct Parent15 { 96 | pub crate1: Crate1, 97 | pub crate2: Crate2, 98 | } 99 | -------------------------------------------------------------------------------- /e2e/workspace/renamed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "renamed" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ts-renamed = { package = "ts-rs", path = "../../../ts-rs" } 8 | -------------------------------------------------------------------------------- /e2e/workspace/renamed/src/main.rs: -------------------------------------------------------------------------------- 1 | use ts_renamed::TS; 2 | 3 | #[derive(TS)] 4 | #[ts(crate = "ts_renamed", export)] 5 | pub struct SimpleStruct { 6 | hello: String, 7 | world: u32, 8 | } 9 | 10 | fn main() { 11 | println!("Hello, world!"); 12 | } 13 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | authors = ["Moritz Bischof "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | ts-rs = { path = "../ts-rs", features = ["serde-compat", "uuid-impl"] } 9 | serde = { version = "1", features = ["derive", "rc"] } 10 | chrono = { version = "0.4", features = ["serde"] } 11 | uuid = { version = "1.1.2", features = ["v4", "serde"] } 12 | -------------------------------------------------------------------------------- /example/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, clippy::disallowed_names)] 2 | 3 | use std::{collections::BTreeSet, rc::Rc}; 4 | 5 | use chrono::NaiveDateTime; 6 | use serde::Serialize; 7 | use ts_rs::TS; 8 | use uuid::Uuid; 9 | 10 | #[derive(Serialize, TS)] 11 | #[ts(rename_all = "lowercase")] 12 | #[ts(export, export_to = "UserRole.ts")] 13 | enum Role { 14 | User, 15 | #[ts(rename = "administrator")] 16 | Admin, 17 | } 18 | 19 | #[derive(Serialize, TS)] 20 | // when 'serde-compat' is enabled, ts-rs tries to use supported serde attributes. 21 | #[serde(rename_all = "UPPERCASE")] 22 | #[ts(export)] 23 | enum Gender { 24 | Male, 25 | Female, 26 | Other, 27 | } 28 | 29 | #[derive(Serialize, TS)] 30 | #[ts(export)] 31 | struct User { 32 | user_id: i32, 33 | first_name: String, 34 | last_name: String, 35 | role: Role, 36 | family: Vec, 37 | #[ts(inline)] 38 | gender: Gender, 39 | token: Uuid, 40 | #[ts(type = "string")] 41 | created_at: NaiveDateTime, 42 | } 43 | 44 | #[derive(Serialize, TS)] 45 | #[serde(tag = "type", rename_all = "snake_case")] 46 | #[ts(export)] 47 | enum Vehicle { 48 | Bicycle { color: String }, 49 | Car { brand: String, color: String }, 50 | } 51 | 52 | #[derive(Serialize, TS)] 53 | #[ts(export)] 54 | struct Point 55 | where 56 | T: TS, 57 | { 58 | time: u64, 59 | value: T, 60 | } 61 | 62 | #[derive(Serialize, TS)] 63 | #[serde(default)] 64 | #[ts(export)] 65 | struct Series { 66 | points: Vec>, 67 | } 68 | 69 | #[derive(Serialize, TS)] 70 | #[serde(tag = "kind", content = "d")] 71 | #[ts(export)] 72 | enum SimpleEnum { 73 | A, 74 | B, 75 | } 76 | 77 | #[derive(Serialize, TS)] 78 | #[serde(tag = "kind", content = "data")] 79 | #[ts(export)] 80 | enum ComplexEnum { 81 | A, 82 | B { foo: String, bar: f64 }, 83 | W(SimpleEnum), 84 | F { nested: SimpleEnum }, 85 | V(Vec), 86 | U(Box), 87 | } 88 | 89 | #[derive(Serialize, TS)] 90 | #[serde(tag = "kind")] 91 | #[ts(export)] 92 | enum InlineComplexEnum { 93 | A, 94 | B { foo: String, bar: f64 }, 95 | W(SimpleEnum), 96 | F { nested: SimpleEnum }, 97 | V(Vec), 98 | U(Box), 99 | } 100 | 101 | #[derive(Serialize, TS)] 102 | #[serde(rename_all = "camelCase")] 103 | #[ts(export)] 104 | struct ComplexStruct { 105 | #[serde(default)] 106 | pub string_tree: Option>>, 107 | } 108 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aleph-Alpha/ts-rs/dc2892f4415d3a8fe46bfddde004692bd46ab33d/logo.png -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ts-rs-macros" 3 | version = "11.0.0" 4 | authors = ["Moritz Bischof "] 5 | edition = "2021" 6 | description = "derive macro for ts-rs" 7 | license = "MIT" 8 | homepage = "https://github.com/Aleph-Alpha/ts-rs" 9 | repository = "https://github.com/Aleph-Alpha/ts-rs" 10 | 11 | [features] 12 | serde-compat = ["termcolor"] 13 | no-serde-warnings = [] 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | proc-macro2 = "1" 20 | quote = "1" 21 | syn = { version = "2.0.28", features = ["full", "extra-traits"] } 22 | termcolor = { version = "1", optional = true } 23 | -------------------------------------------------------------------------------- /macros/src/attr/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | pub use field::*; 4 | use proc_macro2::TokenTree; 5 | use quote::quote; 6 | pub use r#enum::*; 7 | pub use r#struct::*; 8 | use syn::{ 9 | parse::{Parse, ParseStream}, 10 | punctuated::Punctuated, 11 | Error, Expr, Lit, Path, Result, Token, WherePredicate, 12 | }; 13 | pub use variant::*; 14 | mod r#enum; 15 | mod field; 16 | mod r#struct; 17 | mod variant; 18 | 19 | #[derive(Copy, Clone, Debug)] 20 | pub enum Inflection { 21 | Lower, 22 | Upper, 23 | Camel, 24 | Snake, 25 | Pascal, 26 | ScreamingSnake, 27 | Kebab, 28 | ScreamingKebab, 29 | } 30 | 31 | pub(super) trait Attr: Default { 32 | type Item; 33 | 34 | fn merge(self, other: Self) -> Self; 35 | fn assert_validity(&self, item: &Self::Item) -> Result<()>; 36 | } 37 | 38 | pub(super) trait ContainerAttr: Attr { 39 | fn crate_rename(&self) -> Path; 40 | } 41 | 42 | #[derive(Default)] 43 | pub(super) struct Serde(pub T) 44 | where 45 | T: Attr; 46 | 47 | impl Serde 48 | where 49 | T: Attr, 50 | { 51 | pub fn merge(self, other: Self) -> Self { 52 | Self(self.0.merge(other.0)) 53 | } 54 | } 55 | 56 | impl Inflection { 57 | pub fn apply(self, string: &str) -> String { 58 | match self { 59 | Inflection::Lower => string.to_lowercase(), 60 | Inflection::Upper => string.to_uppercase(), 61 | Inflection::Camel => { 62 | let pascal = Inflection::apply(Inflection::Pascal, string); 63 | pascal[..1].to_ascii_lowercase() + &pascal[1..] 64 | } 65 | Inflection::Snake => { 66 | let mut s = String::new(); 67 | 68 | for (i, ch) in string.char_indices() { 69 | if ch.is_uppercase() && i != 0 { 70 | s.push('_'); 71 | } 72 | s.push(ch.to_ascii_lowercase()); 73 | } 74 | 75 | s 76 | } 77 | Inflection::Pascal => { 78 | let mut s = String::with_capacity(string.len()); 79 | 80 | let mut capitalize = true; 81 | for c in string.chars() { 82 | if c == '_' { 83 | capitalize = true; 84 | continue; 85 | } else if capitalize { 86 | s.push(c.to_ascii_uppercase()); 87 | capitalize = false; 88 | } else { 89 | s.push(c) 90 | } 91 | } 92 | 93 | s 94 | } 95 | Inflection::ScreamingSnake => Self::Snake.apply(string).to_ascii_uppercase(), 96 | Inflection::Kebab => Self::Snake.apply(string).replace('_', "-"), 97 | Inflection::ScreamingKebab => Self::Kebab.apply(string).to_ascii_uppercase(), 98 | } 99 | } 100 | } 101 | 102 | fn skip_until_next_comma(input: ParseStream) -> proc_macro2::TokenStream { 103 | input 104 | .step(|cursor| { 105 | let mut stuff = quote!(); 106 | let mut rest = *cursor; 107 | while let Some((tt, next)) = rest.token_tree() { 108 | if let Some((TokenTree::Punct(punct), _)) = next.token_tree() { 109 | if punct.as_char() == ',' { 110 | return Ok((stuff, next)); 111 | } 112 | } 113 | 114 | rest = next; 115 | stuff = quote!(#stuff #tt) 116 | } 117 | 118 | Ok((stuff, rest)) 119 | }) 120 | .unwrap() 121 | } 122 | 123 | fn parse_assign_expr(input: ParseStream) -> Result { 124 | input.parse::()?; 125 | Expr::parse(input) 126 | } 127 | 128 | fn parse_assign_str(input: ParseStream) -> Result { 129 | input.parse::()?; 130 | match Lit::parse(input)? { 131 | Lit::Str(string) => Ok(string.value()), 132 | other => Err(Error::new(other.span(), "expected string")), 133 | } 134 | } 135 | 136 | fn parse_concrete(input: ParseStream) -> Result> { 137 | struct Concrete { 138 | ident: syn::Ident, 139 | _equal_token: Token![=], 140 | ty: syn::Type, 141 | } 142 | 143 | impl Parse for Concrete { 144 | fn parse(input: ParseStream) -> Result { 145 | Ok(Self { 146 | ident: input.parse()?, 147 | _equal_token: input.parse()?, 148 | ty: input.parse()?, 149 | }) 150 | } 151 | } 152 | 153 | let content; 154 | syn::parenthesized!(content in input); 155 | 156 | Ok( 157 | Punctuated::::parse_terminated(&content)? 158 | .into_iter() 159 | .map(|concrete| (concrete.ident, concrete.ty)) 160 | .collect(), 161 | ) 162 | } 163 | 164 | fn parse_assign_inflection(input: ParseStream) -> Result { 165 | input.parse::()?; 166 | 167 | match Lit::parse(input)? { 168 | Lit::Str(string) => Ok(match &*string.value() { 169 | "lowercase" => Inflection::Lower, 170 | "UPPERCASE" => Inflection::Upper, 171 | "camelCase" => Inflection::Camel, 172 | "snake_case" => Inflection::Snake, 173 | "PascalCase" => Inflection::Pascal, 174 | "SCREAMING_SNAKE_CASE" => Inflection::ScreamingSnake, 175 | "kebab-case" => Inflection::Kebab, 176 | "SCREAMING-KEBAB-CASE" => Inflection::ScreamingKebab, 177 | other => { 178 | syn_err!( 179 | string.span(); 180 | r#"Value "{other}" is not valid for "rename_all". Accepted values are: "lowercase", "UPPERCASE", "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", "kebab-case" and "SCREAMING-KEBAB-CASE""# 181 | ) 182 | } 183 | }), 184 | other => Err(Error::new(other.span(), "expected string")), 185 | } 186 | } 187 | 188 | fn parse_assign_from_str(input: ParseStream) -> Result 189 | where 190 | T: Parse, 191 | { 192 | input.parse::()?; 193 | match Lit::parse(input)? { 194 | Lit::Str(string) => string.parse(), 195 | other => Err(Error::new(other.span(), "expected string")), 196 | } 197 | } 198 | 199 | fn parse_bound(input: ParseStream) -> Result> { 200 | input.parse::()?; 201 | match Lit::parse(input)? { 202 | Lit::Str(string) => { 203 | let parser = Punctuated::::parse_terminated; 204 | 205 | Ok(string.parse_with(parser)?.into_iter().collect()) 206 | } 207 | other => Err(Error::new(other.span(), "expected string")), 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /macros/src/attr/struct.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use syn::{parse_quote, Attribute, Expr, Fields, Ident, Path, Result, Type, WherePredicate}; 4 | 5 | use super::{ 6 | parse_assign_expr, parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete, 7 | Attr, ContainerAttr, Serde, Tagged, 8 | }; 9 | use crate::{ 10 | attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr}, 11 | optional::{parse_optional, Optional}, 12 | utils::{extract_docs, parse_attrs}, 13 | }; 14 | 15 | #[derive(Default, Clone)] 16 | pub struct StructAttr { 17 | crate_rename: Option, 18 | pub type_as: Option, 19 | pub type_override: Option, 20 | pub rename_all: Option, 21 | pub rename: Option, 22 | pub export_to: Option, 23 | pub export: bool, 24 | pub tag: Option, 25 | pub docs: Vec, 26 | pub concrete: HashMap, 27 | pub bound: Option>, 28 | pub optional_fields: Optional, 29 | } 30 | 31 | impl StructAttr { 32 | pub fn from_attrs(attrs: &[Attribute]) -> Result { 33 | let mut result = parse_attrs::(attrs)?; 34 | 35 | if cfg!(feature = "serde-compat") { 36 | let serde_attr = crate::utils::parse_serde_attrs::(attrs); 37 | result = result.merge(serde_attr.0); 38 | } 39 | 40 | result.docs = extract_docs(attrs); 41 | 42 | Ok(result) 43 | } 44 | 45 | pub fn from_variant( 46 | enum_attr: &EnumAttr, 47 | variant_attr: &VariantAttr, 48 | variant_fields: &Fields, 49 | ) -> Self { 50 | Self { 51 | crate_rename: Some(enum_attr.crate_rename()), 52 | rename: variant_attr.rename.clone(), 53 | rename_all: variant_attr.rename_all.or(match variant_fields { 54 | Fields::Named(_) => enum_attr.rename_all_fields, 55 | Fields::Unnamed(_) | Fields::Unit => None, 56 | }), 57 | tag: match variant_fields { 58 | Fields::Named(_) => match enum_attr 59 | .tagged() 60 | .expect("The variant attribute is known to be valid at this point") 61 | { 62 | Tagged::Internally { tag } => Some(tag.to_owned()), 63 | _ => None, 64 | }, 65 | _ => None, 66 | }, 67 | 68 | // inline and skip are not supported on StructAttr 69 | ..Self::default() 70 | } 71 | } 72 | } 73 | 74 | impl Attr for StructAttr { 75 | type Item = Fields; 76 | 77 | fn merge(self, other: Self) -> Self { 78 | Self { 79 | crate_rename: self.crate_rename.or(other.crate_rename), 80 | type_as: self.type_as.or(other.type_as), 81 | type_override: self.type_override.or(other.type_override), 82 | rename: self.rename.or(other.rename), 83 | rename_all: self.rename_all.or(other.rename_all), 84 | export_to: self.export_to.or(other.export_to), 85 | export: self.export || other.export, 86 | tag: self.tag.or(other.tag), 87 | docs: other.docs, 88 | concrete: self.concrete.into_iter().chain(other.concrete).collect(), 89 | bound: match (self.bound, other.bound) { 90 | (Some(a), Some(b)) => Some(a.into_iter().chain(b).collect()), 91 | (Some(bound), None) | (None, Some(bound)) => Some(bound), 92 | (None, None) => None, 93 | }, 94 | optional_fields: self.optional_fields.or(other.optional_fields), 95 | } 96 | } 97 | 98 | fn assert_validity(&self, item: &Self::Item) -> Result<()> { 99 | if self.type_override.is_some() { 100 | if self.type_as.is_some() { 101 | syn_err!("`as` is not compatible with `type`"); 102 | } 103 | 104 | if self.rename_all.is_some() { 105 | syn_err!("`rename_all` is not compatible with `type`"); 106 | } 107 | 108 | if self.tag.is_some() { 109 | syn_err!("`tag` is not compatible with `type`"); 110 | } 111 | 112 | if let Optional::Optional { .. } = self.optional_fields { 113 | syn_err!("`optional_fields` is not compatible with `type`"); 114 | } 115 | } 116 | 117 | if self.type_as.is_some() { 118 | if self.tag.is_some() { 119 | syn_err!("`tag` is not compatible with `as`"); 120 | } 121 | 122 | if self.rename_all.is_some() { 123 | syn_err!("`rename_all` is not compatible with `as`"); 124 | } 125 | 126 | if let Optional::Optional { .. } = self.optional_fields { 127 | syn_err!("`optional_fields` is not compatible with `as`"); 128 | } 129 | } 130 | 131 | if !matches!(item, Fields::Named(_)) { 132 | if self.tag.is_some() { 133 | syn_err!("`tag` cannot be used with unit or tuple structs"); 134 | } 135 | 136 | if self.rename_all.is_some() { 137 | syn_err!("`rename_all` cannot be used with unit or tuple structs"); 138 | } 139 | } 140 | 141 | Ok(()) 142 | } 143 | } 144 | 145 | impl ContainerAttr for StructAttr { 146 | fn crate_rename(&self) -> Path { 147 | self.crate_rename 148 | .clone() 149 | .unwrap_or_else(|| parse_quote!(::ts_rs)) 150 | } 151 | } 152 | 153 | impl_parse! { 154 | StructAttr(input, out) { 155 | "crate" => out.crate_rename = Some(parse_assign_from_str(input)?), 156 | "as" => out.type_as = Some(parse_assign_from_str(input)?), 157 | "type" => out.type_override = Some(parse_assign_str(input)?), 158 | "rename" => out.rename = Some(parse_assign_expr(input)?), 159 | "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), 160 | "tag" => out.tag = Some(parse_assign_str(input)?), 161 | "export" => out.export = true, 162 | "export_to" => out.export_to = Some(parse_assign_expr(input)?), 163 | "concrete" => out.concrete = parse_concrete(input)?, 164 | "bound" => out.bound = Some(parse_bound(input)?), 165 | "optional_fields" => out.optional_fields = parse_optional(input)?, 166 | } 167 | } 168 | 169 | impl_parse! { 170 | Serde(input, out) { 171 | "rename" => out.0.rename = Some(parse_assign_expr(input)?), 172 | "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), 173 | "tag" => out.0.tag = Some(parse_assign_str(input)?), 174 | "bound" => out.0.bound = Some(parse_bound(input)?), 175 | // parse #[serde(default)] to not emit a warning 176 | "deny_unknown_fields" | "default" => { 177 | use syn::Token; 178 | if input.peek(Token![=]) { 179 | input.parse::()?; 180 | parse_assign_str(input)?; 181 | } 182 | }, 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /macros/src/attr/variant.rs: -------------------------------------------------------------------------------- 1 | use syn::{Attribute, Expr, Fields, Ident, Result, Type, Variant}; 2 | 3 | use super::{parse_assign_expr, Attr, Serde}; 4 | use crate::{ 5 | attr::{parse_assign_from_str, parse_assign_inflection, parse_assign_str, Inflection}, 6 | utils::parse_attrs, 7 | }; 8 | 9 | #[derive(Default)] 10 | pub struct VariantAttr { 11 | pub type_as: Option, 12 | pub type_override: Option, 13 | pub rename: Option, 14 | pub rename_all: Option, 15 | pub inline: bool, 16 | pub skip: bool, 17 | pub untagged: bool, 18 | } 19 | 20 | impl VariantAttr { 21 | pub fn from_attrs(attrs: &[Attribute]) -> Result { 22 | let mut result = parse_attrs::(attrs)?; 23 | if cfg!(feature = "serde-compat") && !result.skip { 24 | let serde_attr = crate::utils::parse_serde_attrs::(attrs); 25 | result = result.merge(serde_attr.0); 26 | } 27 | Ok(result) 28 | } 29 | } 30 | 31 | impl Attr for VariantAttr { 32 | type Item = Variant; 33 | 34 | fn merge(self, other: Self) -> Self { 35 | Self { 36 | type_as: self.type_as.or(other.type_as), 37 | type_override: self.type_override.or(other.type_override), 38 | rename: self.rename.or(other.rename), 39 | rename_all: self.rename_all.or(other.rename_all), 40 | inline: self.inline || other.inline, 41 | skip: self.skip || other.skip, 42 | untagged: self.untagged || other.untagged, 43 | } 44 | } 45 | 46 | fn assert_validity(&self, item: &Self::Item) -> Result<()> { 47 | if self.type_as.is_some() { 48 | if self.type_override.is_some() { 49 | syn_err_spanned!( 50 | item; 51 | "`as` is not compatible with `type`" 52 | ) 53 | } 54 | 55 | if self.rename_all.is_some() { 56 | syn_err_spanned!( 57 | item; 58 | "`as` is not compatible with `rename_all`" 59 | ) 60 | } 61 | } 62 | 63 | if self.type_override.is_some() { 64 | if self.rename_all.is_some() { 65 | syn_err_spanned!( 66 | item; 67 | "`type` is not compatible with `rename_all`" 68 | ) 69 | } 70 | 71 | if self.inline { 72 | syn_err_spanned!( 73 | item; 74 | "`type` is not compatible with `inline`" 75 | ) 76 | } 77 | } 78 | 79 | if !matches!(item.fields, Fields::Named(_)) && self.rename_all.is_some() { 80 | syn_err_spanned!( 81 | item; 82 | "`rename_all` is not applicable to unit or tuple variants" 83 | ) 84 | } 85 | 86 | Ok(()) 87 | } 88 | } 89 | 90 | impl_parse! { 91 | VariantAttr(input, out) { 92 | "as" => out.type_as = Some(parse_assign_from_str(input)?), 93 | "type" => out.type_override = Some(parse_assign_str(input)?), 94 | "rename" => out.rename = Some(parse_assign_expr(input)?), 95 | "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), 96 | "inline" => out.inline = true, 97 | "skip" => out.skip = true, 98 | "untagged" => out.untagged = true, 99 | } 100 | } 101 | 102 | impl_parse! { 103 | Serde(input, out) { 104 | "rename" => out.0.rename = Some(parse_assign_expr(input)?), 105 | "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), 106 | "skip" => out.0.skip = true, 107 | "untagged" => out.0.untagged = true, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /macros/src/deps.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, rc::Rc}; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, ToTokens}; 5 | use syn::{Path, Type}; 6 | 7 | pub struct Dependencies { 8 | crate_rename: Rc, 9 | dependencies: HashSet, 10 | types: HashSet>, 11 | } 12 | 13 | #[derive(Hash, Eq, PartialEq)] 14 | enum Dependency { 15 | // A dependency on all dependencies of `ty`. 16 | // This does not include a dependency on `ty` itself - only its dependencies! 17 | Transitive { 18 | crate_rename: Rc, 19 | ty: Rc, 20 | }, 21 | // A dependency on all type parameters of `ty`, as returned by `TS::generics()`. 22 | // This does not include a dependency on `ty` itself. 23 | Generics { 24 | crate_rename: Rc, 25 | ty: Rc, 26 | }, 27 | Type(Rc), 28 | } 29 | 30 | impl Dependencies { 31 | pub fn new(crate_rename: Path) -> Self { 32 | Self { 33 | dependencies: HashSet::new(), 34 | crate_rename: Rc::new(crate_rename), 35 | types: HashSet::new(), 36 | } 37 | } 38 | 39 | pub fn used_types(&self) -> impl Iterator { 40 | self.types.iter().map(Rc::as_ref) 41 | } 42 | 43 | /// Adds all dependencies from the given type 44 | pub fn append_from(&mut self, ty: &Type) { 45 | let ty = self.push_type(ty); 46 | self.dependencies.insert(Dependency::Transitive { 47 | crate_rename: self.crate_rename.clone(), 48 | ty: ty.clone(), 49 | }); 50 | } 51 | 52 | /// Adds the given type. 53 | pub fn push(&mut self, ty: &Type) { 54 | let ty = self.push_type(ty); 55 | self.dependencies.insert(Dependency::Type(ty.clone())); 56 | self.dependencies.insert(Dependency::Generics { 57 | crate_rename: self.crate_rename.clone(), 58 | ty: ty.clone(), 59 | }); 60 | } 61 | 62 | pub fn append(&mut self, other: Dependencies) { 63 | self.dependencies.extend(other.dependencies); 64 | self.types.extend(other.types); 65 | } 66 | 67 | fn push_type(&mut self, ty: &Type) -> Rc { 68 | // this can be replaces with `get_or_insert_owned` once #60896 is stabilized 69 | match self.types.get(ty) { 70 | None => { 71 | let ty = Rc::new(ty.clone()); 72 | self.types.insert(ty.clone()); 73 | ty 74 | } 75 | Some(ty) => ty.clone(), 76 | } 77 | } 78 | } 79 | 80 | impl ToTokens for Dependencies { 81 | fn to_tokens(&self, tokens: &mut TokenStream) { 82 | let lines = self.dependencies.iter(); 83 | 84 | tokens.extend(quote![ 85 | #(#lines;)* 86 | ]); 87 | } 88 | } 89 | 90 | impl ToTokens for Dependency { 91 | fn to_tokens(&self, tokens: &mut TokenStream) { 92 | tokens.extend(match self { 93 | Dependency::Transitive { crate_rename, ty } => { 94 | quote![<#ty as #crate_rename::TS>::visit_dependencies(v)] 95 | } 96 | Dependency::Generics { crate_rename, ty } => { 97 | quote![<#ty as #crate_rename::TS>::visit_generics(v)] 98 | } 99 | Dependency::Type(ty) => quote![v.visit::<#ty>()], 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /macros/src/optional.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::{ 3 | ext::IdentExt, parse::ParseStream, parse_quote, parse_quote_spanned, Error, Expr, Ident, Path, 4 | Token, Type, 5 | }; 6 | 7 | use crate::attr::FieldAttr; 8 | 9 | /// Indicates whether the field is marked with `#[ts(optional)]`. 10 | /// `#[ts(optional)]` turns an `t: Option` into `t?: T`, while 11 | /// `#[ts(optional = nullable)]` turns it into `t?: T | null`. 12 | #[derive(Default, Clone, Copy)] 13 | pub enum Optional { 14 | /// Explicitly marked as optional with `#[ts(optional)]` 15 | Optional { nullable: bool }, 16 | 17 | /// Explicitly marked as not optional with `#[ts(optional = false)]` 18 | NotOptional, 19 | 20 | #[default] 21 | Inherit, 22 | } 23 | 24 | impl Optional { 25 | pub fn or(self, other: Optional) -> Self { 26 | match (self, other) { 27 | (Self::Inherit, other) | (other, Self::Inherit) => other, 28 | (Self::Optional { nullable: a }, Self::Optional { nullable: b }) => { 29 | Self::Optional { nullable: a || b } 30 | } 31 | _ => other, 32 | } 33 | } 34 | } 35 | 36 | pub fn parse_optional(input: ParseStream) -> syn::Result { 37 | let optional = if input.peek(Token![=]) { 38 | input.parse::()?; 39 | let span = input.span(); 40 | 41 | match Ident::parse_any(input)?.to_string().as_str() { 42 | "nullable" => Optional::Optional { nullable: true }, 43 | "false" => Optional::NotOptional, 44 | _ => Err(Error::new(span, "expected 'nullable'"))?, 45 | } 46 | } else { 47 | Optional::Optional { nullable: false } 48 | }; 49 | 50 | Ok(optional) 51 | } 52 | 53 | /// Given a field, return a tuple `(is_optional, type)`. 54 | /// 55 | /// `is_optional`: 56 | /// An expression evaluating to bool, indicating whether the field should be annotated with `?`. 57 | /// 58 | /// `type`: 59 | /// The transformed type of the field after applying the `#[ts(optional)]` annotation. 60 | /// This will be either 61 | /// - the unmodified type of the field (no optional or `#[ts(optional = nullable)]`) or 62 | /// - if the field is an `Option`, its inner type `T´ (`#[ts(optional)]`) 63 | pub fn apply( 64 | crate_rename: &Path, 65 | for_struct: Optional, 66 | field_ty: &Type, 67 | attr: &FieldAttr, 68 | span: Span, 69 | ) -> (Expr, Type) { 70 | match (for_struct, attr.optional) { 71 | // explicit `#[ts(optional = false)]` on field, or inherited from struct. 72 | (Optional::NotOptional, Optional::Inherit) | (_, Optional::NotOptional) => { 73 | (parse_quote!(false), field_ty.clone()) 74 | } 75 | // explicit `#[ts(optional)]` on field. 76 | // It takes precedence over the struct attribute, and is enforced **AT COMPILE TIME** 77 | (_, Optional::Optional { nullable }) => ( 78 | // expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`. 79 | parse_quote_spanned! { span => { 80 | fn check_that_field_is_option(_: std::marker::PhantomData) {} 81 | let x: std::marker::PhantomData<#field_ty> = std::marker::PhantomData; 82 | check_that_field_is_option(x); 83 | true 84 | }}, 85 | nullable 86 | .then(|| field_ty.clone()) 87 | .unwrap_or_else(|| unwrap_option(crate_rename, field_ty)), 88 | ), 89 | // Inherited `#[ts(optional)]` from the struct. 90 | // Acts like `#[ts(optional)]` on a field, but does not error on non-`Option` fields. 91 | // Instead, it is a no-op. 92 | (Optional::Optional { nullable }, Optional::Inherit) if attr.type_override.is_none() => ( 93 | parse_quote! { 94 | <#field_ty as #crate_rename::TS>::IS_OPTION 95 | }, 96 | nullable 97 | .then(|| field_ty.clone()) 98 | .unwrap_or_else(|| unwrap_option(crate_rename, field_ty)), 99 | ), 100 | // no applicable `#[ts(optional)]` attributes 101 | _ => { 102 | // field may be omitted during serialization and has a default value, so the field can be 103 | // treated as `#[ts(optional = nullable)]`. 104 | let is_optional = attr.maybe_omitted && attr.has_default; 105 | (parse_quote!(#is_optional), field_ty.clone()) 106 | } 107 | } 108 | } 109 | 110 | /// Unwraps the given option type, turning `Option` into `T`. 111 | /// otherwise, return the provided type as-is. 112 | fn unwrap_option(crate_rename: &Path, ty: &Type) -> Type { 113 | parse_quote! {<#ty as #crate_rename::TS>::OptionInnerType} 114 | } 115 | -------------------------------------------------------------------------------- /macros/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use syn::{ext::IdentExt, Expr, Fields, ItemStruct, Result}; 2 | 3 | use crate::{ 4 | attr::{Attr, StructAttr}, 5 | DerivedTS, 6 | }; 7 | 8 | mod r#enum; 9 | mod named; 10 | mod newtype; 11 | mod tuple; 12 | mod type_as; 13 | mod type_override; 14 | mod unit; 15 | 16 | pub(crate) use r#enum::r#enum_def; 17 | 18 | use crate::utils::make_string_literal; 19 | 20 | pub(crate) fn struct_def(s: &ItemStruct) -> Result { 21 | let attr = StructAttr::from_attrs(&s.attrs)?; 22 | 23 | let ts_name = attr 24 | .rename 25 | .clone() 26 | .unwrap_or_else(|| make_string_literal(&s.ident.unraw().to_string(), s.ident.span())); 27 | type_def(&attr, ts_name, &s.fields) 28 | } 29 | 30 | fn type_def(attr: &StructAttr, ts_name: Expr, fields: &Fields) -> Result { 31 | attr.assert_validity(fields)?; 32 | 33 | if let Some(attr_type_override) = &attr.type_override { 34 | return type_override::type_override_struct(attr, ts_name, attr_type_override); 35 | } 36 | if let Some(attr_type_as) = &attr.type_as { 37 | return type_as::type_as_struct(attr, ts_name, attr_type_as); 38 | } 39 | 40 | match fields { 41 | Fields::Named(named) => match named.named.len() { 42 | 0 if attr.tag.is_none() => Ok(unit::empty_object(attr, ts_name)), 43 | _ => named::named(attr, ts_name, named), 44 | }, 45 | Fields::Unnamed(unnamed) => match unnamed.unnamed.len() { 46 | 0 => Ok(unit::empty_array(attr, ts_name)), 47 | 1 => newtype::newtype(attr, ts_name, unnamed), 48 | _ => tuple::tuple(attr, ts_name, unnamed), 49 | }, 50 | Fields::Unit => Ok(unit::null(attr, ts_name)), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /macros/src/types/named.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{spanned::Spanned, Expr, Field, FieldsNamed, Path, Result}; 4 | 5 | use crate::{ 6 | attr::{Attr, ContainerAttr, FieldAttr, Inflection, StructAttr}, 7 | deps::Dependencies, 8 | optional::Optional, 9 | utils::{raw_name_to_ts_field, to_ts_ident}, 10 | DerivedTS, 11 | }; 12 | 13 | pub(crate) fn named(attr: &StructAttr, ts_name: Expr, fields: &FieldsNamed) -> Result { 14 | let crate_rename = attr.crate_rename(); 15 | 16 | let mut formatted_fields = Vec::new(); 17 | let mut flattened_fields = Vec::new(); 18 | let mut dependencies = Dependencies::new(crate_rename.clone()); 19 | 20 | if let Some(tag) = &attr.tag { 21 | formatted_fields.push(quote! { 22 | format!("\"{}\": \"{}\",", #tag, #ts_name) 23 | }); 24 | } 25 | 26 | for field in &fields.named { 27 | format_field( 28 | &crate_rename, 29 | &mut formatted_fields, 30 | &mut flattened_fields, 31 | &mut dependencies, 32 | field, 33 | &attr.rename_all, 34 | attr.optional_fields, 35 | )?; 36 | } 37 | 38 | let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " ")); 39 | let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & ")); 40 | 41 | let inline = match (formatted_fields.len(), flattened_fields.len()) { 42 | (0, 0) => quote!("{ }".to_owned()), 43 | (_, 0) => quote!(format!("{{ {} }}", #fields)), 44 | (0, 1) => quote! {{ 45 | if #flattened.starts_with('(') && #flattened.ends_with(')') { 46 | #flattened[1..#flattened.len() - 1].trim().to_owned() 47 | } else { 48 | #flattened.trim().to_owned() 49 | } 50 | }}, 51 | (0, _) => quote!(#flattened), 52 | (_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)), 53 | }; 54 | 55 | let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) { 56 | (0, 0) => quote!("{ }".to_owned()), 57 | (_, 0) => quote!(format!("{{ {} }}", #fields)), 58 | (0, _) => quote!(#flattened), 59 | (_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)), 60 | }; 61 | 62 | Ok(DerivedTS { 63 | crate_rename, 64 | // the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it 65 | // results in simpler type definitions. 66 | inline: quote!(#inline.replace(" } & { ", " ")), 67 | inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))), 68 | docs: attr.docs.clone(), 69 | dependencies, 70 | export: attr.export, 71 | export_to: attr.export_to.clone(), 72 | ts_name, 73 | concrete: attr.concrete.clone(), 74 | bound: attr.bound.clone(), 75 | }) 76 | } 77 | 78 | // build an expression which expands to a string, representing a single field of a struct. 79 | // 80 | // formatted_fields will contain all the fields that do not contain the flatten 81 | // attribute, in the format 82 | // key: type, 83 | // 84 | // flattened_fields will contain all the fields that contain the flatten attribute 85 | // in their respective formats, which for a named struct is the same as formatted_fields, 86 | // but for enums is 87 | // ({ /* variant data */ } | { /* variant data */ }) 88 | fn format_field( 89 | crate_rename: &Path, 90 | formatted_fields: &mut Vec, 91 | flattened_fields: &mut Vec, 92 | dependencies: &mut Dependencies, 93 | field: &Field, 94 | rename_all: &Option, 95 | struct_optional: Optional, 96 | ) -> Result<()> { 97 | let field_attr = FieldAttr::from_attrs(&field.attrs)?; 98 | 99 | field_attr.assert_validity(field)?; 100 | 101 | if field_attr.skip { 102 | return Ok(()); 103 | } 104 | 105 | let ty = field_attr.type_as(&field.ty); 106 | 107 | let (is_optional, ty) = crate::optional::apply( 108 | crate_rename, 109 | struct_optional, 110 | &ty, 111 | &field_attr, 112 | field.span(), 113 | ); 114 | let optional_annotation = quote!(if #is_optional { "?" } else { "" }); 115 | 116 | if field_attr.flatten { 117 | flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened())); 118 | dependencies.append_from(&ty); 119 | return Ok(()); 120 | } 121 | 122 | let formatted_ty = field_attr 123 | .type_override 124 | .map(|t| quote!(#t)) 125 | .unwrap_or_else(|| { 126 | if field_attr.inline { 127 | dependencies.append_from(&ty); 128 | quote!(<#ty as #crate_rename::TS>::inline()) 129 | } else { 130 | dependencies.push(&ty); 131 | quote!(<#ty as #crate_rename::TS>::name()) 132 | } 133 | }); 134 | 135 | let field_name = to_ts_ident(field.ident.as_ref().unwrap()); 136 | let name = match (field_attr.rename, rename_all) { 137 | (Some(rn), _) => rn, 138 | (None, Some(rn)) => rn.apply(&field_name), 139 | (None, None) => field_name, 140 | }; 141 | let valid_name = raw_name_to_ts_field(name); 142 | 143 | // Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode 144 | let docs = match &*field_attr.docs { 145 | &[] => quote!(""), 146 | docs => quote!(format!("\n{}", #crate_rename::format_docs(&[#(#docs),*]))), 147 | }; 148 | 149 | formatted_fields.push(quote! { 150 | format!("{}{}{}: {},", #docs, #valid_name, #optional_annotation, #formatted_ty) 151 | }); 152 | 153 | Ok(()) 154 | } 155 | -------------------------------------------------------------------------------- /macros/src/types/newtype.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | use syn::{Expr, FieldsUnnamed, Result}; 3 | 4 | use crate::{ 5 | attr::{Attr, ContainerAttr, FieldAttr, StructAttr}, 6 | deps::Dependencies, 7 | DerivedTS, 8 | }; 9 | 10 | pub(crate) fn newtype( 11 | attr: &StructAttr, 12 | ts_name: Expr, 13 | fields: &FieldsUnnamed, 14 | ) -> Result { 15 | let inner = fields.unnamed.first().unwrap(); 16 | 17 | let field_attr = FieldAttr::from_attrs(&inner.attrs)?; 18 | field_attr.assert_validity(inner)?; 19 | 20 | let crate_rename = attr.crate_rename(); 21 | 22 | if field_attr.skip { 23 | return Ok(super::unit::null(attr, ts_name)); 24 | } 25 | 26 | let inner_ty = field_attr.type_as(&inner.ty); 27 | 28 | let mut dependencies = Dependencies::new(crate_rename.clone()); 29 | 30 | match (&field_attr.type_override, field_attr.inline) { 31 | (Some(_), _) => (), 32 | (None, true) => dependencies.append_from(&inner_ty), 33 | (None, false) => dependencies.push(&inner_ty), 34 | }; 35 | 36 | let inline_def = match field_attr.type_override { 37 | Some(ref o) => quote!(#o.to_owned()), 38 | None if field_attr.inline => quote!(<#inner_ty as #crate_rename::TS>::inline()), 39 | None => quote!(<#inner_ty as #crate_rename::TS>::name()), 40 | }; 41 | 42 | Ok(DerivedTS { 43 | crate_rename, 44 | inline: inline_def, 45 | inline_flattened: None, 46 | docs: attr.docs.clone(), 47 | dependencies, 48 | export: attr.export, 49 | export_to: attr.export_to.clone(), 50 | ts_name, 51 | concrete: attr.concrete.clone(), 52 | bound: attr.bound.clone(), 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /macros/src/types/tuple.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{spanned::Spanned, Expr, Field, FieldsUnnamed, Path, Result}; 4 | 5 | use crate::{ 6 | attr::{Attr, ContainerAttr, FieldAttr, StructAttr}, 7 | deps::Dependencies, 8 | optional::Optional, 9 | DerivedTS, 10 | }; 11 | 12 | pub(crate) fn tuple(attr: &StructAttr, ts_name: Expr, fields: &FieldsUnnamed) -> Result { 13 | let crate_rename = attr.crate_rename(); 14 | let mut formatted_fields = Vec::new(); 15 | let mut dependencies = Dependencies::new(crate_rename.clone()); 16 | for field in &fields.unnamed { 17 | format_field( 18 | &crate_rename, 19 | &mut formatted_fields, 20 | &mut dependencies, 21 | field, 22 | attr.optional_fields, 23 | )?; 24 | } 25 | 26 | Ok(DerivedTS { 27 | crate_rename, 28 | inline: quote! { 29 | format!( 30 | "[{}]", 31 | [#(#formatted_fields),*].join(", ") 32 | ) 33 | }, 34 | inline_flattened: None, 35 | docs: attr.docs.clone(), 36 | dependencies, 37 | export: attr.export, 38 | export_to: attr.export_to.clone(), 39 | ts_name, 40 | concrete: attr.concrete.clone(), 41 | bound: attr.bound.clone(), 42 | }) 43 | } 44 | 45 | fn format_field( 46 | crate_rename: &Path, 47 | formatted_fields: &mut Vec, 48 | dependencies: &mut Dependencies, 49 | field: &Field, 50 | struct_optional: Optional, 51 | ) -> Result<()> { 52 | let field_attr = FieldAttr::from_attrs(&field.attrs)?; 53 | field_attr.assert_validity(field)?; 54 | 55 | if field_attr.skip { 56 | return Ok(()); 57 | } 58 | 59 | let ty = field_attr.type_as(&field.ty); 60 | let (is_optional, ty) = crate::optional::apply( 61 | crate_rename, 62 | struct_optional, 63 | &ty, 64 | &field_attr, 65 | field.span(), 66 | ); 67 | 68 | let formatted_ty = field_attr 69 | .type_override 70 | .map(|t| quote!(#t.to_owned())) 71 | .unwrap_or_else(|| { 72 | if field_attr.inline { 73 | dependencies.append_from(&ty); 74 | quote!(<#ty as #crate_rename::TS>::inline()) 75 | } else { 76 | dependencies.push(&ty); 77 | quote!(<#ty as #crate_rename::TS>::name()) 78 | } 79 | }); 80 | 81 | formatted_fields.push(quote! { 82 | if #is_optional { 83 | format!("({})?", #formatted_ty) 84 | } else { 85 | #formatted_ty 86 | } 87 | }); 88 | 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /macros/src/types/type_as.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | use syn::{Expr, Result, Type}; 3 | 4 | use crate::{ 5 | attr::{ContainerAttr, EnumAttr, StructAttr}, 6 | deps::Dependencies, 7 | DerivedTS, 8 | }; 9 | 10 | pub(crate) fn type_as_struct( 11 | attr: &StructAttr, 12 | ts_name: Expr, 13 | type_as: &Type, 14 | ) -> Result { 15 | let crate_rename = attr.crate_rename(); 16 | 17 | let mut dependencies = Dependencies::new(crate_rename.clone()); 18 | dependencies.append_from(type_as); 19 | 20 | Ok(DerivedTS { 21 | crate_rename: crate_rename.clone(), 22 | inline: quote!(<#type_as as #crate_rename::TS>::inline()), 23 | inline_flattened: None, 24 | docs: attr.docs.clone(), 25 | dependencies, 26 | export: attr.export, 27 | export_to: attr.export_to.clone(), 28 | ts_name, 29 | concrete: attr.concrete.clone(), 30 | bound: attr.bound.clone(), 31 | }) 32 | } 33 | 34 | pub(crate) fn type_as_enum(attr: &EnumAttr, ts_name: Expr, type_as: &Type) -> Result { 35 | let crate_rename = attr.crate_rename(); 36 | 37 | let mut dependencies = Dependencies::new(crate_rename.clone()); 38 | dependencies.append_from(type_as); 39 | 40 | Ok(DerivedTS { 41 | crate_rename: crate_rename.clone(), 42 | inline: quote!(<#type_as as #crate_rename::TS>::inline()), 43 | inline_flattened: None, 44 | docs: attr.docs.clone(), 45 | dependencies, 46 | export: attr.export, 47 | export_to: attr.export_to.clone(), 48 | ts_name, 49 | concrete: attr.concrete.clone(), 50 | bound: attr.bound.clone(), 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /macros/src/types/type_override.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | use syn::{Expr, Result}; 3 | 4 | use crate::{ 5 | attr::{ContainerAttr, EnumAttr, StructAttr}, 6 | deps::Dependencies, 7 | DerivedTS, 8 | }; 9 | 10 | pub(crate) fn type_override_struct( 11 | attr: &StructAttr, 12 | ts_name: Expr, 13 | type_override: &str, 14 | ) -> Result { 15 | let crate_rename = attr.crate_rename(); 16 | 17 | Ok(DerivedTS { 18 | crate_rename: crate_rename.clone(), 19 | inline: quote!(#type_override.to_owned()), 20 | inline_flattened: None, 21 | docs: attr.docs.clone(), 22 | dependencies: Dependencies::new(crate_rename), 23 | export: attr.export, 24 | export_to: attr.export_to.clone(), 25 | ts_name, 26 | concrete: attr.concrete.clone(), 27 | bound: attr.bound.clone(), 28 | }) 29 | } 30 | 31 | pub(crate) fn type_override_enum( 32 | attr: &EnumAttr, 33 | ts_name: Expr, 34 | type_override: &str, 35 | ) -> Result { 36 | let crate_rename = attr.crate_rename(); 37 | 38 | Ok(DerivedTS { 39 | crate_rename: crate_rename.clone(), 40 | inline: quote!(#type_override.to_owned()), 41 | inline_flattened: None, 42 | docs: attr.docs.clone(), 43 | dependencies: Dependencies::new(crate_rename), 44 | export: attr.export, 45 | export_to: attr.export_to.clone(), 46 | ts_name, 47 | concrete: attr.concrete.clone(), 48 | bound: attr.bound.clone(), 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /macros/src/types/unit.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | use syn::Expr; 3 | 4 | use crate::{ 5 | attr::{ContainerAttr, StructAttr}, 6 | deps::Dependencies, 7 | DerivedTS, 8 | }; 9 | 10 | pub(crate) fn empty_object(attr: &StructAttr, ts_name: Expr) -> DerivedTS { 11 | let crate_rename = attr.crate_rename(); 12 | 13 | DerivedTS { 14 | crate_rename: crate_rename.clone(), 15 | inline: quote!("Record".to_owned()), 16 | inline_flattened: None, 17 | docs: attr.docs.clone(), 18 | dependencies: Dependencies::new(crate_rename), 19 | export: attr.export, 20 | export_to: attr.export_to.clone(), 21 | ts_name, 22 | concrete: attr.concrete.clone(), 23 | bound: attr.bound.clone(), 24 | } 25 | } 26 | 27 | pub(crate) fn empty_array(attr: &StructAttr, ts_name: Expr) -> DerivedTS { 28 | let crate_rename = attr.crate_rename(); 29 | 30 | DerivedTS { 31 | crate_rename: crate_rename.clone(), 32 | inline: quote!("never[]".to_owned()), 33 | inline_flattened: None, 34 | docs: attr.docs.clone(), 35 | dependencies: Dependencies::new(crate_rename), 36 | export: attr.export, 37 | export_to: attr.export_to.clone(), 38 | ts_name, 39 | concrete: attr.concrete.clone(), 40 | bound: attr.bound.clone(), 41 | } 42 | } 43 | 44 | pub(crate) fn null(attr: &StructAttr, ts_name: Expr) -> DerivedTS { 45 | let crate_rename = attr.crate_rename(); 46 | 47 | DerivedTS { 48 | crate_rename: crate_rename.clone(), 49 | inline: quote!("null".to_owned()), 50 | inline_flattened: None, 51 | docs: attr.docs.clone(), 52 | dependencies: Dependencies::new(crate_rename), 53 | export: attr.export, 54 | export_to: attr.export_to.clone(), 55 | ts_name, 56 | concrete: attr.concrete.clone(), 57 | bound: attr.bound.clone(), 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "StdExternalCrate" -------------------------------------------------------------------------------- /ts-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ts-rs" 3 | version = "11.0.0" 4 | authors = ["Moritz Bischof "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "generate typescript bindings from rust types" 8 | homepage = "https://github.com/Aleph-Alpha/ts-rs" 9 | repository = "https://github.com/Aleph-Alpha/ts-rs" 10 | keywords = ["typescript", "ts", "bindings", "ts-rs", "wasm"] 11 | categories = [ 12 | "development-tools::ffi", 13 | "development-tools::build-utils", 14 | "wasm", 15 | "web-programming", 16 | ] 17 | readme = "../README.md" 18 | rust-version = "1.78.0" 19 | 20 | [features] 21 | chrono-impl = ["chrono"] 22 | bigdecimal-impl = ["bigdecimal"] 23 | uuid-impl = ["uuid"] 24 | bson-uuid-impl = ["bson"] 25 | bytes-impl = ["bytes"] 26 | url-impl = ["url"] 27 | serde-compat = ["ts-rs-macros/serde-compat"] 28 | format = ["dprint-plugin-typescript"] 29 | default = ["serde-compat"] 30 | indexmap-impl = ["indexmap"] 31 | ordered-float-impl = ["ordered-float"] 32 | heapless-impl = ["heapless"] 33 | semver-impl = ["semver"] 34 | smol_str-impl = ["smol_str"] 35 | serde-json-impl = ["serde_json"] 36 | no-serde-warnings = ["ts-rs-macros/no-serde-warnings"] 37 | import-esm = [] 38 | tokio-impl = ["tokio"] 39 | 40 | [dev-dependencies] 41 | serde = { version = "1.0", features = ["derive"] } 42 | serde_json = "1" 43 | chrono = { version = "0.4", features = ["serde"] } 44 | tokio = { version = "1.40", features = ["sync", "rt"] } 45 | 46 | [dependencies] 47 | ts-rs-macros = { version = "=11.0.0", path = "../macros" } 48 | thiserror = "2" 49 | 50 | heapless = { version = ">= 0.7, < 0.9", optional = true } 51 | dprint-plugin-typescript = { version = "0.90", optional = true } 52 | chrono = { version = "0.4", optional = true } 53 | bigdecimal = { version = ">= 0.0.13, < 0.5", features = ["serde"], optional = true } 54 | uuid = { version = "1", optional = true } 55 | bson = { version = "2", optional = true } 56 | bytes = { version = "1", optional = true } 57 | url = { version = "2", optional = true } 58 | semver = { version = "1", optional = true } 59 | smol_str = { version = "0.3", optional = true } 60 | indexmap = { version = "2", optional = true } 61 | ordered-float = { version = ">= 3, < 6", optional = true } 62 | serde_json = { version = "1", optional = true } 63 | tokio = { version = "1", features = ["sync"], optional = true } 64 | -------------------------------------------------------------------------------- /ts-rs/src/chrono.rs: -------------------------------------------------------------------------------- 1 | // we want to implement TS for deprecated types as well 2 | #![allow(deprecated)] 3 | 4 | use chrono::{ 5 | Date, DateTime, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, NaiveTime, 6 | TimeZone, Utc, Weekday, 7 | }; 8 | 9 | use super::{impl_primitives, TS}; 10 | 11 | macro_rules! impl_dummy { 12 | ($($t:ty),*) => {$( 13 | impl TS for $t { 14 | type WithoutGenerics = $t; 15 | type OptionInnerType = Self; 16 | 17 | fn name() -> String { String::new() } 18 | fn inline() -> String { String::new() } 19 | fn inline_flattened() -> String { panic!("{} cannot be flattened", ::name()) } 20 | fn decl() -> String { panic!("{} cannot be declared", ::name()) } 21 | fn decl_concrete() -> String { panic!("{} cannot be declared", ::name()) } 22 | } 23 | )*}; 24 | } 25 | 26 | impl_primitives!(NaiveDateTime, NaiveDate, NaiveTime, Month, Weekday, Duration => "string"); 27 | impl_dummy!(Utc, Local, FixedOffset); 28 | 29 | impl TS for DateTime { 30 | type WithoutGenerics = Self; 31 | type OptionInnerType = Self; 32 | 33 | fn ident() -> String { 34 | "string".to_owned() 35 | } 36 | fn name() -> String { 37 | "string".to_owned() 38 | } 39 | fn inline() -> String { 40 | "string".to_owned() 41 | } 42 | fn inline_flattened() -> String { 43 | panic!("{} cannot be flattened", ::name()) 44 | } 45 | fn decl() -> String { 46 | panic!("{} cannot be declared", ::name()) 47 | } 48 | fn decl_concrete() -> String { 49 | panic!("{} cannot be declared", ::name()) 50 | } 51 | } 52 | 53 | impl TS for Date { 54 | type WithoutGenerics = Self; 55 | type OptionInnerType = Self; 56 | 57 | fn ident() -> String { 58 | "string".to_owned() 59 | } 60 | fn name() -> String { 61 | "string".to_owned() 62 | } 63 | fn inline() -> String { 64 | "string".to_owned() 65 | } 66 | fn inline_flattened() -> String { 67 | panic!("{} cannot be flattened", ::name()) 68 | } 69 | fn decl() -> String { 70 | panic!("{} cannot be declared", ::name()) 71 | } 72 | fn decl_concrete() -> String { 73 | panic!("{} cannot be declared", ::name()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ts-rs/src/export/error.rs: -------------------------------------------------------------------------------- 1 | /// An error which may occur when exporting a type 2 | #[derive(thiserror::Error, Debug)] 3 | pub enum ExportError { 4 | #[error("this type cannot be exported")] 5 | CannotBeExported(&'static str), 6 | #[cfg(feature = "format")] 7 | #[error("an error occurred while formatting the generated typescript output")] 8 | Formatting(String), 9 | #[error("an error occurred while performing IO")] 10 | Io(#[from] std::io::Error), 11 | #[error("the environment variable CARGO_MANIFEST_DIR is not set")] 12 | ManifestDirNotSet, 13 | #[error("an error occurred while writing to a formatted buffer")] 14 | Fmt(#[from] std::fmt::Error), 15 | } 16 | -------------------------------------------------------------------------------- /ts-rs/src/export/path.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Component as C, Path, PathBuf}; 2 | 3 | use super::ExportError as E; 4 | 5 | const ERROR_MESSAGE: &str = r#"The path provided with `#[ts(export_to = "..")]` is not valid"#; 6 | 7 | pub fn absolute>(path: T) -> Result { 8 | let path = std::env::current_dir()?.join(path.as_ref()); 9 | 10 | let mut out = Vec::new(); 11 | for comp in path.components() { 12 | match comp { 13 | C::CurDir => (), 14 | C::ParentDir => { 15 | out.pop().ok_or(E::CannotBeExported(ERROR_MESSAGE))?; 16 | } 17 | comp => out.push(comp), 18 | } 19 | } 20 | 21 | Ok(if !out.is_empty() { 22 | out.iter().collect() 23 | } else { 24 | PathBuf::from(".") 25 | }) 26 | } 27 | 28 | // Construct a relative path from a provided base directory path to the provided path. 29 | // 30 | // Copyright 2012-2015 The Rust Project Developers. 31 | // 32 | // Licensed under the Apache License, Version 2.0 or the MIT license 34 | // , at your 35 | // option. This file may not be copied, modified, or distributed 36 | // except according to those terms. 37 | // 38 | // Adapted from rustc's path_relative_from 39 | // https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158 40 | pub(super) fn diff_paths(path: P, base: B) -> Result 41 | where 42 | P: AsRef, 43 | B: AsRef, 44 | { 45 | let path = absolute(path)?; 46 | let base = absolute(base)?; 47 | 48 | let mut ita = path.components(); 49 | let mut itb = base.components(); 50 | let mut comps: Vec = vec![]; 51 | 52 | loop { 53 | match (ita.next(), itb.next()) { 54 | (Some(C::ParentDir | C::CurDir), _) | (_, Some(C::ParentDir | C::CurDir)) => { 55 | unreachable!( 56 | "The paths have been cleaned, no no '.' or '..' components are present" 57 | ) 58 | } 59 | (None, None) => break, 60 | (Some(a), None) => { 61 | comps.push(a); 62 | comps.extend(ita.by_ref()); 63 | break; 64 | } 65 | (None, _) => comps.push(C::ParentDir), 66 | (Some(a), Some(b)) if comps.is_empty() && a == b => (), 67 | (Some(a), Some(_)) => { 68 | comps.push(C::ParentDir); 69 | for _ in itb { 70 | comps.push(C::ParentDir); 71 | } 72 | comps.push(a); 73 | comps.extend(ita.by_ref()); 74 | break; 75 | } 76 | } 77 | } 78 | 79 | Ok(comps.iter().map(|c| c.as_os_str()).collect()) 80 | } 81 | -------------------------------------------------------------------------------- /ts-rs/src/serde_json.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use super::{impl_primitives, impl_shadow, TS}; 4 | 5 | #[derive(TS)] 6 | #[ts( 7 | crate = "crate", 8 | rename = "JsonValue", 9 | untagged, 10 | export_to = "serde_json/" 11 | )] 12 | pub enum TsJsonValue { 13 | Number(i32), 14 | String(String), 15 | Boolean(bool), 16 | Array(Vec), 17 | Object(HashMap), 18 | Null(()), 19 | } 20 | 21 | impl_shadow!(as TsJsonValue: impl TS for serde_json::Value); 22 | impl_primitives!(serde_json::Number => "number"); 23 | impl_shadow!(as HashMap: impl TS for serde_json::Map); 24 | -------------------------------------------------------------------------------- /ts-rs/src/tokio.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::{Mutex, OnceCell, RwLock}; 2 | 3 | use super::{impl_wrapper, TypeVisitor, TS}; 4 | 5 | impl_wrapper!(impl TS for Mutex); 6 | impl_wrapper!(impl TS for OnceCell); 7 | impl_wrapper!(impl TS for RwLock); 8 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/arrays.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "arrays/")] 7 | struct Interface { 8 | a: [i32; 4], 9 | } 10 | 11 | #[test] 12 | fn free() { 13 | assert_eq!(<[String; 4]>::inline(), "[string, string, string, string]") 14 | } 15 | 16 | #[test] 17 | fn interface() { 18 | assert_eq!( 19 | Interface::inline(), 20 | "{ a: [number, number, number, number], }" 21 | ) 22 | } 23 | 24 | #[test] 25 | fn newtype() { 26 | #[derive(TS)] 27 | struct Newtype(#[allow(dead_code)] [i32; 4]); 28 | 29 | assert_eq!(Newtype::inline(), "[number, number, number, number]") 30 | } 31 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/bound.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | trait Driver { 6 | type Info; 7 | } 8 | 9 | struct TsDriver; 10 | 11 | #[derive(TS)] 12 | struct TsInfo; 13 | 14 | impl Driver for TsDriver { 15 | type Info = TsInfo; 16 | } 17 | 18 | #[derive(TS)] 19 | #[ts(export, export_to = "bound/")] 20 | #[ts(concrete(D = TsDriver))] 21 | struct Inner { 22 | info: D::Info, 23 | } 24 | 25 | #[derive(TS)] 26 | #[ts(export, export_to = "bound/")] 27 | #[ts(concrete(D = TsDriver), bound = "D::Info: TS")] 28 | struct Outer { 29 | inner: Inner, 30 | } 31 | 32 | #[test] 33 | fn test_bound() { 34 | assert_eq!(Outer::::decl(), "type Outer = { inner: Inner, };"); 35 | assert_eq!(Inner::::decl(), "type Inner = { info: TsInfo, };"); 36 | } 37 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/bson.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "bson-uuid-impl")] 2 | 3 | use bson::{oid::ObjectId, Uuid}; 4 | use ts_rs::TS; 5 | 6 | #[derive(TS)] 7 | #[ts(export, export_to = "bson/")] 8 | struct User { 9 | _id: ObjectId, 10 | _uuid: Uuid, 11 | } 12 | 13 | #[test] 14 | fn bson() { 15 | assert_eq!(User::decl(), "type User = { _id: string, _uuid: string, };") 16 | } 17 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/chrono.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated, dead_code)] 2 | #![cfg(feature = "chrono-impl")] 3 | 4 | use chrono::{ 5 | Date, DateTime, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, NaiveTime, Utc, 6 | Weekday, 7 | }; 8 | use ts_rs::TS; 9 | 10 | #[derive(TS)] 11 | #[ts(export, export_to = "chrono/")] 12 | struct Chrono { 13 | date: (NaiveDate, Date, Date, Date), 14 | time: NaiveTime, 15 | date_time: ( 16 | NaiveDateTime, 17 | DateTime, 18 | DateTime, 19 | DateTime, 20 | ), 21 | duration: Duration, 22 | month: Month, 23 | weekday: Weekday, 24 | } 25 | 26 | #[test] 27 | fn chrono() { 28 | assert_eq!( 29 | Chrono::decl(), 30 | "type Chrono = { date: [string, string, string, string], time: string, date_time: [string, string, string, string], duration: string, month: string, weekday: string, };" 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/complex_flattened_type.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | /// Defines the type of input and its intial fields 4 | #[derive(TS)] 5 | #[ts(tag = "input_type")] 6 | pub enum InputType { 7 | Text, 8 | Expression, 9 | Number { 10 | min: Option, 11 | max: Option, 12 | }, 13 | Dropdown { 14 | options: Vec<(String, String)>, 15 | }, 16 | } 17 | 18 | #[derive(TS)] 19 | #[ts(tag = "type")] 20 | pub enum InputFieldElement { 21 | Label { 22 | text: String, 23 | }, 24 | Input { 25 | #[ts(flatten)] 26 | input: InputType, 27 | name: Option, 28 | placeholder: Option, 29 | default: Option, 30 | }, 31 | } 32 | 33 | #[derive(TS)] 34 | #[ts(export, export_to = "complex_flattened_type/")] 35 | pub struct InputField { 36 | #[ts(flatten)] 37 | r#type: InputFieldElement, 38 | } 39 | 40 | #[test] 41 | fn complex_flattened_type() { 42 | assert_eq!( 43 | InputFieldElement::decl(), 44 | r#"type InputFieldElement = { "type": "Label", text: string, } | { "type": "Input", name: string | null, placeholder: string | null, default: string | null, } & ({ "input_type": "Text" } | { "input_type": "Expression" } | { "input_type": "Number", min: number | null, max: number | null, } | { "input_type": "Dropdown", options: Array<[string, string]>, });"# 45 | ); 46 | assert_eq!( 47 | InputField::decl(), 48 | r#"type InputField = { "type": "Label", text: string, } | { "type": "Input", name: string | null, placeholder: string | null, default: string | null, } & ({ "input_type": "Text" } | { "input_type": "Expression" } | { "input_type": "Number", min: number | null, max: number | null, } | { "input_type": "Dropdown", options: Array<[string, string]>, });"# 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/concrete_generic.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | mod issue_261 { 4 | use ts_rs::TS; 5 | 6 | trait Driver { 7 | type Info; 8 | } 9 | 10 | struct TsDriver; 11 | impl Driver for TsDriver { 12 | type Info = String; 13 | } 14 | 15 | #[derive(TS)] 16 | #[ts(export, export_to = "concrete_generic/issue_261/")] 17 | struct OtherInfo { 18 | x: i32, 19 | } 20 | 21 | #[derive(TS)] 22 | #[ts(export, export_to = "concrete_generic/issue_261/")] 23 | struct OtherDriver; 24 | impl Driver for OtherDriver { 25 | type Info = OtherInfo; 26 | } 27 | 28 | #[derive(TS)] 29 | #[ts(export, export_to = "concrete_generic/issue_261/", concrete(T = TsDriver))] 30 | struct Consumer1 { 31 | info: T::Info, 32 | } 33 | 34 | #[derive(TS)] 35 | #[ts(export, export_to = "concrete_generic/issue_261/", concrete(T = OtherDriver))] 36 | struct Consumer2 { 37 | info: T::Info, 38 | driver: T, 39 | } 40 | 41 | #[test] 42 | fn concrete_generic_param() { 43 | assert_eq!( 44 | Consumer1::::decl(), 45 | "type Consumer1 = { info: string, };" 46 | ); 47 | // `decl` must use the concrete generic, no matter what we pass in 48 | assert_eq!( 49 | Consumer1::::decl(), 50 | Consumer1::::decl() 51 | ); 52 | 53 | assert_eq!( 54 | Consumer2::::decl_concrete(), 55 | "type Consumer2 = { info: OtherInfo, driver: OtherDriver, };" 56 | ); 57 | } 58 | } 59 | 60 | mod simple { 61 | use ts_rs::TS; 62 | 63 | #[derive(TS)] 64 | #[ts(export, export_to = "concrete_generic/simple/")] 65 | #[ts(concrete(T = i32))] 66 | struct Simple { 67 | t: T, 68 | } 69 | 70 | #[derive(TS)] 71 | #[ts(export, export_to = "concrete_generic/simple/")] 72 | struct Tuple { 73 | f: Option, 74 | } 75 | 76 | #[derive(TS)] 77 | #[ts(export, export_to = "concrete_generic/simple/")] 78 | #[ts(concrete(T = i32))] 79 | struct WithOption { 80 | opt: Option, 81 | } 82 | 83 | #[test] 84 | fn simple() { 85 | assert_eq!(Simple::::decl(), "type Simple = { t: number, };"); 86 | assert_eq!( 87 | WithOption::::decl(), 88 | "type WithOption = { opt: number | null, };" 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/enum_flattening.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Serialize; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 9 | #[ts(export, export_to = "enum_flattening/externally_tagged/")] 10 | struct FooExternally { 11 | qux: i32, 12 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 13 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 14 | baz: BarExternally, 15 | biz: Option, 16 | } 17 | 18 | #[derive(TS)] 19 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 20 | #[ts(export, export_to = "enum_flattening/externally_tagged/")] 21 | enum BarExternally { 22 | Baz { a: i32, a2: String }, 23 | Biz { b: bool }, 24 | Buz { c: String, d: Option }, 25 | } 26 | 27 | #[derive(TS)] 28 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 29 | #[ts(export, export_to = "enum_flattening/externally_tagged/")] 30 | struct NestedExternally { 31 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 32 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 33 | a: FooExternally, 34 | u: u32, 35 | } 36 | 37 | #[test] 38 | fn externally_tagged() { 39 | assert_eq!( 40 | FooExternally::inline(), 41 | r#"{ qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"# 42 | ); 43 | assert_eq!( 44 | NestedExternally::inline(), 45 | r#"{ u: number, qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"# 46 | ); 47 | } 48 | 49 | #[derive(TS)] 50 | #[ts(export, export_to = "enum_flattening/adjacently_tagged/")] 51 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 52 | struct FooAdjecently { 53 | one: i32, 54 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 55 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 56 | baz: BarAdjecently, 57 | qux: Option, 58 | } 59 | 60 | #[derive(TS)] 61 | #[ts(export, export_to = "enum_flattening/adjacently_tagged/")] 62 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 63 | #[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "stuff"))] 64 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "stuff"))] 65 | enum BarAdjecently { 66 | Baz { 67 | a: i32, 68 | a2: String, 69 | }, 70 | Biz { 71 | b: bool, 72 | }, 73 | 74 | #[cfg_attr(feature = "serde-compat", serde(untagged))] 75 | #[cfg_attr(not(feature = "serde-compat"), ts(untagged))] 76 | Buz { 77 | c: String, 78 | d: Option, 79 | }, 80 | } 81 | 82 | #[derive(TS)] 83 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 84 | struct NestedAdjecently { 85 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 86 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 87 | a: FooAdjecently, 88 | u: u32, 89 | } 90 | 91 | #[test] 92 | fn adjacently_tagged() { 93 | assert_eq!( 94 | FooAdjecently::inline(), 95 | r#"{ one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"# 96 | ); 97 | assert_eq!( 98 | NestedAdjecently::inline(), 99 | r#"{ u: number, one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"# 100 | ); 101 | } 102 | 103 | #[derive(TS)] 104 | #[ts(export, export_to = "enum_flattening/internally_tagged/")] 105 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 106 | struct FooInternally { 107 | qux: Option, 108 | 109 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 110 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 111 | baz: BarInternally, 112 | } 113 | 114 | #[derive(TS)] 115 | #[ts(export, export_to = "enum_flattening/internally_tagged/")] 116 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 117 | #[cfg_attr(feature = "serde-compat", serde(tag = "type"))] 118 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))] 119 | enum BarInternally { 120 | Baz { a: i32, a2: String }, 121 | Biz { b: bool }, 122 | Buz { c: String, d: Option }, 123 | } 124 | 125 | #[derive(TS)] 126 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 127 | struct NestedInternally { 128 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 129 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 130 | a: FooInternally, 131 | u: u32, 132 | } 133 | 134 | #[test] 135 | fn internally_tagged() { 136 | assert_eq!( 137 | FooInternally::inline(), 138 | r#"{ qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"# 139 | ); 140 | assert_eq!( 141 | NestedInternally::inline(), 142 | r#"{ u: number, qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"# 143 | ); 144 | } 145 | 146 | #[derive(TS)] 147 | #[ts(export, export_to = "enum_flattening/untagged/")] 148 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 149 | struct FooUntagged { 150 | one: u32, 151 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 152 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 153 | baz: BarUntagged, 154 | } 155 | 156 | #[derive(TS)] 157 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 158 | struct NestedUntagged { 159 | #[cfg_attr(feature = "serde-compat", serde(flatten))] 160 | #[cfg_attr(not(feature = "serde-compat"), ts(flatten))] 161 | a: FooUntagged, 162 | u: u32, 163 | } 164 | 165 | #[derive(TS)] 166 | #[ts(export, export_to = "enum_flattening/untagged/")] 167 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 168 | #[cfg_attr(feature = "serde-compat", serde(untagged))] 169 | #[cfg_attr(not(feature = "serde-compat"), ts(untagged))] 170 | enum BarUntagged { 171 | Baz { a: i32, a2: String }, 172 | Biz { b: bool }, 173 | Buz { c: String }, 174 | } 175 | 176 | #[test] 177 | fn untagged() { 178 | assert_eq!( 179 | FooUntagged::inline(), 180 | r#"{ one: number, } & ({ a: number, a2: string, } | { b: boolean, } | { c: string, })"# 181 | ); 182 | assert_eq!( 183 | NestedUntagged::inline(), 184 | r#"{ u: number, one: number, } & ({ a: number, a2: string, } | { b: boolean, } | { c: string, })"# 185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/enum_struct_rename_all.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde-compat")] 2 | use serde::Serialize; 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "enum_struct_rename_all/")] 7 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 8 | #[cfg_attr(feature = "serde-compat", serde(rename_all = "camelCase"))] 9 | #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "camelCase"))] 10 | pub enum TaskStatus { 11 | #[cfg_attr(feature = "serde-compat", serde(rename_all = "camelCase"))] 12 | #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "camelCase"))] 13 | Running { started_time: String }, 14 | 15 | #[cfg_attr(feature = "serde-compat", serde(rename_all = "camelCase"))] 16 | #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "camelCase"))] 17 | Terminated { 18 | status: i32, 19 | stdout: String, 20 | stderr: String, 21 | }, 22 | } 23 | 24 | #[test] 25 | pub fn enum_struct_rename_all() { 26 | assert_eq!( 27 | TaskStatus::inline(), 28 | r#"{ "running": { startedTime: string, } } | { "terminated": { status: number, stdout: string, stderr: string, } }"# 29 | ) 30 | } 31 | 32 | #[derive(TS, Clone)] 33 | #[ts(export, export_to = "enum_struct_rename_all/")] 34 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 35 | #[cfg_attr(feature = "serde-compat", serde(rename_all_fields = "kebab-case"))] 36 | #[cfg_attr(not(feature = "serde-compat"), ts(rename_all_fields = "kebab-case"))] 37 | pub enum TaskStatus2 { 38 | Running { 39 | started_time: String, 40 | }, 41 | 42 | Terminated { 43 | status: i32, 44 | stdout: String, 45 | stderr: String, 46 | }, 47 | 48 | A(i32), 49 | B(i32, i32), 50 | C, 51 | } 52 | 53 | #[test] 54 | pub fn enum_struct_rename_all_fields() { 55 | assert_eq!( 56 | TaskStatus2::inline(), 57 | r#"{ "Running": { "started-time": string, } } | { "Terminated": { status: number, stdout: string, stderr: string, } } | { "A": number } | { "B": [number, number] } | "C""# 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/enum_variant_annotation.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Serialize; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "enum_variant_anotation/")] 9 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 10 | #[cfg_attr(feature = "serde-compat", serde(rename_all = "SCREAMING_SNAKE_CASE"))] 11 | #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "SCREAMING_SNAKE_CASE"))] 12 | enum A { 13 | MessageOne { 14 | sender_id: String, 15 | number_of_snakes: u64, 16 | }, 17 | #[cfg_attr(feature = "serde-compat", serde(rename_all = "camelCase"))] 18 | #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "camelCase"))] 19 | MessageTwo { 20 | sender_id: String, 21 | number_of_camels: u64, 22 | }, 23 | } 24 | 25 | #[test] 26 | fn test_enum_variant_rename_all() { 27 | assert_eq!( 28 | A::inline(), 29 | r#"{ "MESSAGE_ONE": { sender_id: string, number_of_snakes: bigint, } } | { "MESSAGE_TWO": { senderId: string, numberOfCamels: bigint, } }"#, 30 | ); 31 | } 32 | 33 | #[derive(TS)] 34 | #[ts(export, export_to = "enum_variant_anotation/")] 35 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 36 | enum B { 37 | #[cfg_attr(feature = "serde-compat", serde(rename = "SnakeMessage"))] 38 | #[cfg_attr(not(feature = "serde-compat"), ts(rename = "SnakeMessage"))] 39 | MessageOne { 40 | sender_id: String, 41 | number_of_snakes: u64, 42 | }, 43 | #[cfg_attr(feature = "serde-compat", serde(rename = "CamelMessage"))] 44 | #[cfg_attr(not(feature = "serde-compat"), ts(rename = "CamelMessage"))] 45 | MessageTwo { 46 | sender_id: String, 47 | number_of_camels: u64, 48 | }, 49 | } 50 | 51 | #[test] 52 | fn test_enum_variant_rename() { 53 | assert_eq!( 54 | B::inline(), 55 | r#"{ "SnakeMessage": { sender_id: string, number_of_snakes: bigint, } } | { "CamelMessage": { sender_id: string, number_of_camels: bigint, } }"#, 56 | ); 57 | } 58 | 59 | #[derive(TS)] 60 | #[ts(export, export_to = "enum_variant_anotation/")] 61 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 62 | #[cfg_attr(feature = "serde-compat", serde(tag = "kind"))] 63 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "kind"))] 64 | pub enum C { 65 | #[cfg_attr(feature = "serde-compat", serde(rename = "SQUARE_THING"))] 66 | #[cfg_attr(not(feature = "serde-compat"), ts(rename = "SQUARE_THING"))] 67 | SquareThing { 68 | name: String, 69 | // ... 70 | }, 71 | } 72 | 73 | #[test] 74 | fn test_enum_variant_with_tag() { 75 | assert_eq!(C::inline(), r#"{ "kind": "SQUARE_THING", name: string, }"#); 76 | } 77 | 78 | #[cfg(feature = "serde-compat")] 79 | #[test] 80 | fn test_tag_and_content_quoted() { 81 | #[derive(Serialize, TS)] 82 | #[serde(tag = "kebab-cased-tag", content = "whitespace in content")] 83 | enum E { 84 | V { f: String }, 85 | } 86 | assert_eq!( 87 | E::inline(), 88 | r#"{ "kebab-cased-tag": "V", "whitespace in content": { f: string, } }"# 89 | ) 90 | } 91 | 92 | #[cfg(feature = "serde-compat")] 93 | #[test] 94 | fn test_variant_quoted() { 95 | #[derive(Serialize, TS)] 96 | #[serde(rename_all = "kebab-case")] 97 | enum E { 98 | VariantName { f: String }, 99 | } 100 | assert_eq!(E::inline(), r#"{ "variant-name": { f: string, } }"#) 101 | } 102 | 103 | #[derive(TS)] 104 | #[ts(export, export_to = "enum_variant_anotation/")] 105 | enum D { 106 | Foo {}, 107 | } 108 | 109 | #[derive(TS)] 110 | #[ts(export, export_to = "enum_variant_anotation/", tag = "type")] 111 | enum E { 112 | Foo {}, 113 | Bar {}, 114 | Biz { x: i32 }, 115 | } 116 | 117 | #[test] 118 | fn test_empty_struct_variant_with_tag() { 119 | assert_eq!( 120 | E::inline(), 121 | r#"{ "type": "Foo", } | { "type": "Bar", } | { "type": "Biz", x: number, }"# 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/export_manually.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{concat, fs}; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export_to = "export_manually/UserFile.ts")] 9 | struct User { 10 | name: String, 11 | age: i32, 12 | active: bool, 13 | } 14 | 15 | #[derive(TS)] 16 | #[ts(export_to = "export_manually/dir/")] 17 | struct UserDir { 18 | name: String, 19 | age: i32, 20 | active: bool, 21 | } 22 | 23 | #[test] 24 | fn export_manually() { 25 | User::export().unwrap(); 26 | 27 | let expected_content = if cfg!(feature = "format") { 28 | concat!( 29 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n", 30 | "export type User = { name: string; age: number; active: boolean };\n", 31 | ) 32 | } else { 33 | concat!( 34 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", 35 | "\nexport type User = { name: string, age: number, active: boolean, };", 36 | "\n", 37 | ) 38 | }; 39 | 40 | let actual_content = fs::read_to_string(User::default_output_path().unwrap()).unwrap(); 41 | 42 | assert_eq!(actual_content, expected_content); 43 | } 44 | 45 | #[test] 46 | fn export_manually_dir() { 47 | UserDir::export().unwrap(); 48 | 49 | let expected_content = if cfg!(feature = "format") { 50 | concat!( 51 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n", 52 | "export type UserDir = { name: string; age: number; active: boolean };\n", 53 | ) 54 | } else { 55 | concat!( 56 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", 57 | "\nexport type UserDir = { name: string, age: number, active: boolean, };", 58 | "\n", 59 | ) 60 | }; 61 | 62 | let actual_content = fs::read_to_string(UserDir::default_output_path().unwrap()).unwrap(); 63 | 64 | assert_eq!(actual_content, expected_content); 65 | } 66 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/export_to.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "export_to/with_str_to_file.ts")] 7 | struct WithStrToFile; 8 | 9 | #[derive(TS)] 10 | #[ts(export, export_to = "export_to/")] 11 | struct WithStrToDir; 12 | 13 | // -- 14 | 15 | #[derive(TS)] 16 | #[ts(export, export_to = &"export_to/with_str_ref_to_file.ts")] 17 | struct WithStrRefToFile; 18 | 19 | #[derive(TS)] 20 | #[ts(export, export_to = &"export_to/")] 21 | struct WithStrRefToDir; 22 | 23 | // -- 24 | 25 | #[derive(TS)] 26 | #[ts(export, export_to = format!("export_to/with_string_to_file.ts"))] 27 | struct WithStringToFile; 28 | 29 | #[derive(TS)] 30 | #[ts(export, export_to = format!("export_to/"))] 31 | struct WithStringToDir; 32 | 33 | // -- 34 | 35 | #[derive(TS)] 36 | #[ts(export, export_to = &format!("export_to/with_string_ref_to_file.ts"))] 37 | struct WithStringRefToFile; 38 | 39 | #[derive(TS)] 40 | #[ts(export, export_to = &format!("export_to/"))] 41 | struct WithStringRefToDir; 42 | 43 | // -- 44 | 45 | #[derive(TS)] 46 | #[ts(export, export_to = { 47 | let dir = WithStrToFile::default_output_path().unwrap(); 48 | let dir = dir.parent().unwrap(); 49 | let file = dir.join("to_absolute_file_path.ts"); 50 | let file = std::path::absolute(file).unwrap(); 51 | file.display().to_string() 52 | })] 53 | struct ToAbsoluteFilePath(WithStrToDir, WithStrToFile); 54 | 55 | #[derive(TS)] 56 | #[ts(export, export_to = { 57 | let dir = WithStrToFile::default_output_path().unwrap(); 58 | let dir = dir.parent().unwrap(); 59 | let dir = std::path::absolute(dir).unwrap(); 60 | let dir = dir.display(); 61 | format!("{dir}/") 62 | })] 63 | struct ToAbsoluteDirPath(WithStrToDir, WithStrToFile, ToAbsoluteFilePath); 64 | 65 | // -- 66 | 67 | #[test] 68 | #[cfg(test)] 69 | fn check_export_complete() { 70 | export_bindings_withstrtofile(); 71 | export_bindings_withstrtodir(); 72 | export_bindings_withstrreftofile(); 73 | export_bindings_withstrreftodir(); 74 | export_bindings_withstringtofile(); 75 | export_bindings_withstringtodir(); 76 | export_bindings_withstringreftofile(); 77 | export_bindings_withstringreftodir(); 78 | export_bindings_toabsolutefilepath(); 79 | export_bindings_toabsolutedirpath(); 80 | 81 | let files = [ 82 | "with_str_to_file.ts", 83 | "WithStrToDir.ts", 84 | "with_str_ref_to_file.ts", 85 | "WithStrRefToDir.ts", 86 | "with_string_to_file.ts", 87 | "WithStringToDir.ts", 88 | "with_string_ref_to_file.ts", 89 | "WithStringRefToDir.ts", 90 | "to_absolute_file_path.ts", 91 | "ToAbsoluteDirPath.ts", 92 | ]; 93 | 94 | let dir = std::env::var("TS_RS_EXPORT_DIR").unwrap_or_else(|_| "./bindings".to_owned()); 95 | let dir = Path::new(&dir).join("export_to"); 96 | 97 | files 98 | .iter() 99 | .map(|file| dir.join(file)) 100 | .for_each(|file| assert!(file.is_file(), "{file:?}")); 101 | } 102 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/field_rename.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[cfg_attr(feature = "serde-compat", derive(serde::Serialize, serde::Deserialize))] 7 | struct Rename { 8 | #[cfg_attr( 9 | feature = "serde-compat", 10 | serde(rename = "c", skip_serializing_if = "String::is_empty") 11 | )] 12 | a: String, 13 | #[ts(rename = "bb")] 14 | b: i32, 15 | } 16 | 17 | #[test] 18 | fn test() { 19 | if (cfg!(feature = "serde-compat")) { 20 | assert_eq!(Rename::inline(), "{ c: string, bb: number, }") 21 | } else { 22 | assert_eq!(Rename::inline(), "{ a: string, bb: number, }") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/flatten.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::collections::HashMap; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "flatten/")] 9 | struct A { 10 | a: i32, 11 | b: i32, 12 | #[ts(flatten)] 13 | c: HashMap, 14 | } 15 | 16 | #[derive(TS)] 17 | #[ts(export, export_to = "flatten/")] 18 | struct B { 19 | #[ts(flatten)] 20 | a: A, 21 | c: i32, 22 | } 23 | 24 | #[derive(TS)] 25 | #[ts(export, export_to = "flatten/")] 26 | struct C { 27 | #[ts(inline)] 28 | b: B, 29 | d: i32, 30 | } 31 | 32 | #[test] 33 | fn test_def() { 34 | assert_eq!( 35 | C::inline(), 36 | "{ b: { c: number, a: number, b: number, } & ({ [key in string]?: number }), d: number, }" 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/generic_fields.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, clippy::box_collection)] 2 | 3 | use std::borrow::Cow; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "generic_fields/")] 9 | struct Newtype(Vec>); 10 | 11 | #[test] 12 | fn newtype() { 13 | assert_eq!(Newtype::inline(), "Array"); 14 | } 15 | 16 | #[derive(TS)] 17 | #[ts(export, export_to = "generic_fields/")] 18 | struct NewtypeNested(Vec>); 19 | 20 | #[test] 21 | fn newtype_nested() { 22 | assert_eq!(NewtypeNested::inline(), "Array>"); 23 | } 24 | 25 | #[test] 26 | fn alias() { 27 | type Alias = Vec; 28 | assert_eq!(Alias::inline(), "Array"); 29 | } 30 | 31 | #[test] 32 | fn alias_nested() { 33 | type Alias = Vec>; 34 | assert_eq!(Alias::inline(), "Array>"); 35 | } 36 | 37 | #[derive(TS)] 38 | #[ts(export, export_to = "generic_fields/")] 39 | struct Struct { 40 | a: Box>, 41 | b: (Vec, Vec), 42 | c: [Vec; 3], 43 | } 44 | 45 | #[test] 46 | fn named() { 47 | assert_eq!( 48 | Struct::inline(), 49 | "{ a: Array, b: [Array, Array], c: [Array, Array, Array], }" 50 | ); 51 | } 52 | 53 | #[derive(TS)] 54 | #[ts(export, export_to = "generic_fields/")] 55 | struct StructNested { 56 | a: Vec>, 57 | b: (Vec>, Vec>), 58 | c: [Vec>; 3], 59 | } 60 | 61 | #[test] 62 | fn named_nested() { 63 | assert_eq!(StructNested::inline(), "{ a: Array>, b: [Array>, Array>], c: [Array>, Array>, Array>], }"); 64 | } 65 | 66 | #[derive(TS)] 67 | #[ts(export, export_to = "generic_fields/")] 68 | struct Tuple(Vec, (Vec, Vec), [Vec; 3]); 69 | 70 | #[test] 71 | fn tuple() { 72 | assert_eq!( 73 | Tuple::inline(), 74 | "[Array, [Array, Array], [Array, Array, Array]]" 75 | ); 76 | } 77 | 78 | #[derive(TS)] 79 | #[ts(export, export_to = "generic_fields/")] 80 | struct TupleNested( 81 | Vec>, 82 | (Vec>, Vec>), 83 | [Vec>; 3], 84 | ); 85 | 86 | #[test] 87 | fn tuple_nested() { 88 | assert_eq!( 89 | TupleNested::inline(), 90 | "[Array>, [Array>, Array>], [Array>, Array>, Array>]]" 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/generic_without_import.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[derive(ts_rs::TS)] 4 | struct Test { 5 | field: T, 6 | } 7 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/generics_flatten.rs: -------------------------------------------------------------------------------- 1 | use ts_rs_macros::TS; 2 | 3 | // https://github.com/Aleph-Alpha/ts-rs/issues/335 4 | #[derive(TS)] 5 | #[ts(export, export_to = "generics/flatten/")] 6 | struct Item { 7 | id: String, 8 | #[ts(flatten)] 9 | inner: D, 10 | } 11 | 12 | #[derive(TS)] 13 | #[ts(export, export_to = "generics/flatten/")] 14 | struct TwoParameters { 15 | id: String, 16 | #[ts(flatten)] 17 | a: A, 18 | #[ts(flatten)] 19 | b: B, 20 | ab: (A, B), 21 | } 22 | 23 | #[derive(TS)] 24 | #[ts(export, export_to = "generics/flatten/")] 25 | enum Enum { 26 | A { 27 | #[ts(flatten)] 28 | a: A, 29 | }, 30 | B { 31 | #[ts(flatten)] 32 | b: B, 33 | }, 34 | AB(A, B), 35 | } 36 | 37 | #[test] 38 | fn flattened_generic_parameters() { 39 | use ts_rs::TS; 40 | 41 | #[derive(TS)] 42 | struct Inner { 43 | x: i32, 44 | } 45 | 46 | assert_eq!(Item::<()>::decl(), "type Item = { id: string, } & D;"); 47 | assert_eq!( 48 | TwoParameters::<(), ()>::decl(), 49 | "type TwoParameters = { id: string, ab: [A, B], } & A & B;" 50 | ); 51 | assert_eq!( 52 | Enum::<(), ()>::decl(), 53 | "type Enum = { \"A\": A } | { \"B\": B } | { \"AB\": [A, B] };" 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/hashmap.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::collections::{BTreeMap, HashMap, HashSet}; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "hashmap/")] 9 | struct Hashes { 10 | map: HashMap, 11 | set: HashSet, 12 | } 13 | 14 | #[test] 15 | fn hashmap() { 16 | assert_eq!( 17 | Hashes::decl(), 18 | "type Hashes = { map: { [key in string]?: string }, set: Array, };" 19 | ) 20 | } 21 | 22 | struct CustomHasher {} 23 | 24 | type CustomHashMap = HashMap; 25 | type CustomHashSet = HashSet; 26 | 27 | #[derive(TS)] 28 | #[ts(export, export_to = "hashmap/")] 29 | struct HashesHasher { 30 | map: CustomHashMap, 31 | set: CustomHashSet, 32 | } 33 | 34 | #[test] 35 | fn hashmap_with_custom_hasher() { 36 | assert_eq!( 37 | HashesHasher::decl(), 38 | "type HashesHasher = { map: { [key in string]?: string }, set: Array, };" 39 | ) 40 | } 41 | 42 | #[derive(TS, Eq, PartialEq, Hash)] 43 | #[ts(export, export_to = "hashmap/")] 44 | struct CustomKey(String); 45 | 46 | #[derive(TS)] 47 | #[ts(export, export_to = "hashmap/")] 48 | struct CustomValue; 49 | 50 | #[derive(TS)] 51 | #[ts(export, export_to = "hashmap/")] 52 | struct HashMapWithCustomTypes { 53 | map: HashMap, 54 | } 55 | 56 | #[derive(TS)] 57 | #[ts(export, export_to = "hashmap/")] 58 | struct BTreeMapWithCustomTypes { 59 | map: BTreeMap, 60 | } 61 | 62 | #[test] 63 | fn with_custom_types() { 64 | assert_eq!( 65 | HashMapWithCustomTypes::inline(), 66 | BTreeMapWithCustomTypes::inline() 67 | ); 68 | assert_eq!( 69 | HashMapWithCustomTypes::decl(), 70 | "type HashMapWithCustomTypes = { map: { [key in CustomKey]?: CustomValue }, };" 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/hashset.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::collections::{BTreeSet, HashSet}; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS, Eq, PartialEq, Hash)] 8 | #[ts(export, export_to = "hashset/")] 9 | struct CustomValue; 10 | 11 | #[derive(TS)] 12 | #[ts(export, export_to = "hashset/")] 13 | struct HashSetWithCustomType { 14 | set: HashSet, 15 | } 16 | 17 | #[derive(TS)] 18 | #[ts(export, export_to = "hashset/")] 19 | struct BTreeSetWithCustomType { 20 | set: BTreeSet, 21 | } 22 | 23 | #[test] 24 | fn with_custom_types() { 25 | assert_eq!( 26 | HashSetWithCustomType::inline(), 27 | BTreeSetWithCustomType::inline() 28 | ); 29 | assert_eq!( 30 | HashSetWithCustomType::decl(), 31 | "type HashSetWithCustomType = { set: Array, };" 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/impl_primitive.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "bigdecimal-impl")] 2 | #[test] 3 | fn impl_primitive_bigdecimal() { 4 | assert_eq!( 5 | ::name(), 6 | ::name() 7 | ); 8 | assert_eq!( 9 | ::inline(), 10 | ::inline() 11 | ) 12 | } 13 | 14 | #[cfg(feature = "smol_str-impl")] 15 | #[test] 16 | fn impl_primitive_smolstr() { 17 | assert_eq!( 18 | ::name(), 19 | ::name() 20 | ); 21 | assert_eq!( 22 | ::inline(), 23 | ::inline() 24 | ) 25 | } 26 | 27 | #[cfg(feature = "uuid-impl")] 28 | #[test] 29 | fn impl_primitive_uuid() { 30 | assert_eq!( 31 | ::name(), 32 | ::name() 33 | ); 34 | assert_eq!( 35 | ::inline(), 36 | ::inline() 37 | ) 38 | } 39 | 40 | #[cfg(feature = "url-impl")] 41 | #[test] 42 | fn impl_primitive_url() { 43 | assert_eq!( 44 | ::name(), 45 | ::name() 46 | ); 47 | assert_eq!( 48 | ::inline(), 49 | ::inline() 50 | ) 51 | } 52 | 53 | #[cfg(feature = "ordered-float-impl")] 54 | #[test] 55 | fn impl_primitive_order_float() { 56 | assert_eq!( 57 | as ts_rs::TS>::name(), 58 | ::name() 59 | ); 60 | assert_eq!( 61 | as ts_rs::TS>::inline(), 62 | ::inline() 63 | ); 64 | assert_eq!( 65 | as ts_rs::TS>::name(), 66 | ::name() 67 | ); 68 | assert_eq!( 69 | as ts_rs::TS>::inline(), 70 | ::inline() 71 | ) 72 | } 73 | 74 | #[cfg(feature = "bson-uuid-impl")] 75 | #[test] 76 | fn impl_primitive_bson_uuid() { 77 | assert_eq!( 78 | ::name(), 79 | ::name() 80 | ); 81 | assert_eq!( 82 | ::inline(), 83 | ::inline() 84 | ); 85 | assert_eq!( 86 | ::name(), 87 | ::name() 88 | ); 89 | assert_eq!( 90 | ::inline(), 91 | ::inline() 92 | ) 93 | } 94 | 95 | #[cfg(feature = "semver-impl")] 96 | #[test] 97 | fn impl_primitive_semver() { 98 | assert_eq!( 99 | ::name(), 100 | ::name() 101 | ); 102 | assert_eq!( 103 | ::inline(), 104 | ::inline() 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/imports.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export_to = "imports/ts_rs_test_type_a.ts")] 7 | pub struct TestTypeA { 8 | value: T, 9 | } 10 | 11 | #[derive(TS)] 12 | #[ts(export_to = "imports/ts_rs_test_type_b.ts")] 13 | pub struct TestTypeB { 14 | value: T, 15 | } 16 | 17 | #[derive(TS)] 18 | #[ts(export_to = "imports/")] 19 | pub enum TestEnum { 20 | C { value: TestTypeB }, 21 | A1 { value: TestTypeA }, 22 | A2 { value: TestTypeA }, 23 | } 24 | 25 | #[test] 26 | fn test_def() { 27 | // The only way to get access to how the imports look is to export the type and load the exported file 28 | TestEnum::export_all().unwrap(); 29 | let text = std::fs::read_to_string(TestEnum::default_output_path().unwrap()).unwrap(); 30 | 31 | let expected = match (cfg!(feature = "format"), cfg!(feature = "import-esm")) { 32 | (true, true) => concat!( 33 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", 34 | "import type { TestTypeA } from \"./ts_rs_test_type_a.js\";\n", 35 | "import type { TestTypeB } from \"./ts_rs_test_type_b.js\";\n", 36 | "\n", 37 | "export type TestEnum = { \"C\": { value: TestTypeB } } | {\n", 38 | " \"A1\": { value: TestTypeA };\n", 39 | "} | { \"A2\": { value: TestTypeA } };\n", 40 | ), 41 | (true, false) => concat!( 42 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", 43 | "import type { TestTypeA } from \"./ts_rs_test_type_a\";\n", 44 | "import type { TestTypeB } from \"./ts_rs_test_type_b\";\n", 45 | "\n", 46 | "export type TestEnum = { \"C\": { value: TestTypeB } } | {\n", 47 | " \"A1\": { value: TestTypeA };\n", 48 | "} | { \"A2\": { value: TestTypeA } };\n", 49 | ), 50 | (false, true) => concat!( 51 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", 52 | "import type { TestTypeA } from \"./ts_rs_test_type_a.js\";\n", 53 | "import type { TestTypeB } from \"./ts_rs_test_type_b.js\";\n", 54 | "\n", 55 | "export type TestEnum = { \"C\": { value: TestTypeB, } } | { \"A1\": { value: TestTypeA, } } | { \"A2\": { value: TestTypeA, } };", 56 | "\n", 57 | ), 58 | (false, false) => concat!( 59 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", 60 | "import type { TestTypeA } from \"./ts_rs_test_type_a\";\n", 61 | "import type { TestTypeB } from \"./ts_rs_test_type_b\";\n", 62 | "\n", 63 | "export type TestEnum = { \"C\": { value: TestTypeB, } } | { \"A1\": { value: TestTypeA, } } | { \"A2\": { value: TestTypeA, } };", 64 | "\n", 65 | ), 66 | }; 67 | 68 | assert_eq!(text, expected); 69 | } 70 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/indexmap.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![cfg(feature = "indexmap-impl")] 3 | 4 | use indexmap::{IndexMap, IndexSet}; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "indexmap/")] 9 | struct Indexes { 10 | map: IndexMap, 11 | set: IndexSet, 12 | } 13 | 14 | #[test] 15 | fn indexmap() { 16 | assert_eq!( 17 | Indexes::decl(), 18 | "type Indexes = { map: { [key in string]?: string }, set: Array, };" 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/infer_as.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | trait Bar { 6 | type Baz; 7 | } 8 | 9 | impl Bar for String { 10 | type Baz = i32; 11 | } 12 | 13 | #[derive(TS)] 14 | #[ts(export)] 15 | struct Foo { 16 | #[ts(optional, as = "Option<_>")] 17 | my_optional_bool: bool, 18 | 19 | #[ts(as = "<_ as Bar>::Baz")] 20 | q_self: String, 21 | } 22 | 23 | #[test] 24 | fn test() { 25 | assert_eq!( 26 | Foo::inline(), 27 | "{ my_optional_bool?: boolean, q_self: number, }" 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_168.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::collections::HashMap; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "issue_168/")] 9 | pub struct Foo { 10 | map: HashMap, 11 | } 12 | 13 | #[derive(TS)] 14 | #[ts(export, export_to = "issue_168/")] 15 | pub struct FooInlined { 16 | #[ts(inline)] 17 | map: HashMap, 18 | } 19 | 20 | #[derive(TS)] 21 | #[ts(export, export_to = "issue_168/")] 22 | struct Bar { 23 | #[ts(inline)] 24 | map: HashMap, 25 | } 26 | 27 | #[derive(TS)] 28 | #[ts(export, export_to = "issue_168/")] 29 | struct Baz { 30 | #[ts(inline)] 31 | map: HashMap, 32 | } 33 | 34 | #[test] 35 | #[cfg(not(feature = "import-esm"))] 36 | fn issue_168() { 37 | assert_eq!( 38 | FooInlined::export_to_string().unwrap(), 39 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\ 40 | \n\ 41 | export type FooInlined = { map: { [key in number]?: { map: { [key in number]?: { map: { [key in number]?: string }, } }, } }, };\n" 42 | ); 43 | assert_eq!( 44 | Foo::export_to_string().unwrap(), 45 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\ 46 | import type { Bar } from \"./Bar\";\n\ 47 | \n\ 48 | export type Foo = { map: { [key in number]?: Bar }, };\n" 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_232.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "issue_232/")] 7 | struct State { 8 | a: Result, 9 | b: Result, 10 | } 11 | 12 | #[derive(TS)] 13 | #[ts(export, export_to = "issue_232/")] 14 | struct StateInlined { 15 | #[ts(inline)] 16 | a: Result, 17 | #[ts(inline)] 18 | b: Result, 19 | } 20 | 21 | #[derive(TS)] 22 | #[ts(export, export_to = "issue_232/")] 23 | struct StateInlinedVec { 24 | #[ts(inline)] 25 | a: Vec>, 26 | #[ts(inline)] 27 | b: Vec>, 28 | } 29 | 30 | #[derive(TS)] 31 | #[ts(export, export_to = "issue_232/")] 32 | struct EnumWithName { 33 | name: String, 34 | inner: Enum, 35 | } 36 | 37 | #[derive(TS)] 38 | #[ts(export, export_to = "issue_232/")] 39 | enum Enum { 40 | A, 41 | B, 42 | } 43 | 44 | #[test] 45 | #[cfg(not(feature = "import-esm"))] 46 | fn issue_232() { 47 | println!("{}", StateInlinedVec::export_to_string().unwrap()); 48 | assert_eq!( 49 | StateInlined::export_to_string().unwrap(), 50 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\ 51 | import type { Enum } from \"./Enum\";\n\ 52 | \n\ 53 | export type StateInlined = { \ 54 | a: { Ok : { name: string, inner: Enum, } } | { Err : string }, \ 55 | b: { Ok : { name: string, inner: Enum, } } | { Err : string }, \ 56 | };\n" 57 | ); 58 | assert_eq!( 59 | State::export_to_string().unwrap(), 60 | "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\ 61 | import type { EnumWithName } from \"./EnumWithName\";\n\ 62 | \n\ 63 | export type State = { \ 64 | a: { Ok : EnumWithName } | { Err : string }, \ 65 | b: { Ok : EnumWithName } | { Err : string }, \ 66 | };\n" 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_308.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use ts_rs::{Dependency, ExportError, TypeVisitor, TS}; 4 | 5 | #[rustfmt::skip] 6 | trait Malicious { 7 | type WithoutGenerics: TS + ?Sized; 8 | const DOCS: Option<&'static str> = None; 9 | 10 | fn ident() -> String { unimplemented!() } 11 | fn decl() -> String { unimplemented!() } 12 | fn decl_concrete() -> String { unimplemented!() } 13 | fn name() -> String { unimplemented!() } 14 | fn inline() -> String { unimplemented!() } 15 | fn inline_flattened() -> String { unimplemented!() } 16 | fn dependencies() -> Vec { unimplemented!() } 17 | fn visit_dependencies(_: &mut impl TypeVisitor) { unimplemented!() } 18 | fn visit_generics(_: &mut impl TypeVisitor) { unimplemented!() } 19 | fn export() -> Result<(), ExportError> { unimplemented!() } 20 | fn export_all() -> Result<(), ExportError> { unimplemented!() } 21 | fn export_all_to(out_dir: impl AsRef) -> Result<(), ExportError> { unimplemented!() } 22 | fn export_to_string() -> Result { unimplemented!() } 23 | fn output_path() -> Option { unimplemented!() } 24 | fn default_output_path() -> Option { unimplemented!() } 25 | } 26 | 27 | impl Malicious for T { 28 | type WithoutGenerics = (); 29 | } 30 | 31 | #[derive(TS)] 32 | #[ts(export, export_to = "issue_308/")] 33 | struct MyStruct(A, B); 34 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_317.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | #[derive(TS)] 4 | #[ts(export_to = "issue_317/")] 5 | struct VariantId(u32); 6 | 7 | #[derive(TS)] 8 | #[ts(export_to = "issue_317/")] 9 | struct VariantOverview { 10 | id: u32, 11 | name: String, 12 | } 13 | 14 | #[derive(TS)] 15 | #[ts(export, export_to = "issue_317/")] 16 | struct Container { 17 | variants: Vec<(VariantId, VariantOverview)>, 18 | } 19 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_338.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "issue_338/")] 7 | pub struct MyType { 8 | pub my_field_0: bool, 9 | pub my_field_1: HashMap, 10 | } 11 | 12 | #[derive(TS)] 13 | #[ts(export, export_to = "issue_338/")] 14 | pub enum MyEnum { 15 | Variant0, 16 | Variant1, 17 | Variant2, 18 | Variant3, 19 | } 20 | 21 | #[derive(TS)] 22 | #[ts(export, export_to = "issue_338/")] 23 | pub struct MyStruct { 24 | pub my_field_0: bool, 25 | pub my_field_1: u32, 26 | pub my_field_2: Option, 27 | pub my_field_3: Option, 28 | pub my_field_4: Option, 29 | pub my_field_5: String, 30 | } 31 | 32 | #[test] 33 | fn test() { 34 | assert_eq!( 35 | MyType::inline(), 36 | "{ my_field_0: boolean, my_field_1: { [key in MyEnum]?: MyStruct }, }" 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_397.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! get_model_name { 3 | () => {{ 4 | let mut module = module_path!().rsplit_once("::").unwrap().1.to_owned(); 5 | module[0..1].make_ascii_uppercase(); 6 | format!("{module}Model") 7 | }}; 8 | } 9 | 10 | mod entities { 11 | mod users { 12 | use ts_rs::TS; 13 | 14 | #[derive(TS)] 15 | #[ts(export)] 16 | #[ts(export_to = "issue_397/")] 17 | #[ts(rename = { 18 | let mut module = module_path!().rsplit_once("::").unwrap().1.to_owned(); 19 | module[0..1].make_ascii_uppercase(); 20 | format!("{module}Model") 21 | })] 22 | struct Model; 23 | } 24 | 25 | mod posts { 26 | use ts_rs::TS; 27 | 28 | #[derive(TS)] 29 | #[ts(export)] 30 | #[ts(export_to = "issue_397/")] 31 | #[ts(rename = get_model_name!())] 32 | struct Model; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_415.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde-compat")] 2 | 3 | use ts_rs::TS; 4 | 5 | struct Foreign; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "issue_415/")] 9 | struct Issue415 { 10 | #[ts(optional, type = "Date")] 11 | a: Option, 12 | } 13 | 14 | #[test] 15 | fn issue_415() { 16 | assert_eq!(Issue415::decl(), "type Issue415 = { a?: Date, };"); 17 | } 18 | 19 | #[derive(TS)] 20 | #[ts(export, export_to = "issue_415/")] 21 | struct InTuple(i32, #[ts(optional, type = "Date")] Option); 22 | 23 | #[test] 24 | fn in_tuple() { 25 | assert_eq!(InTuple::decl(), "type InTuple = [number, (Date)?];"); 26 | } 27 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_70.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::collections::HashMap; 4 | 5 | use ts_rs::TS; 6 | 7 | type TypeAlias = HashMap; 8 | 9 | #[derive(TS)] 10 | #[ts(export, export_to = "issue_70/")] 11 | enum Enum { 12 | A(TypeAlias), 13 | B(HashMap), 14 | } 15 | 16 | #[derive(TS)] 17 | #[ts(export, export_to = "issue_70/")] 18 | struct Struct { 19 | a: TypeAlias, 20 | b: HashMap, 21 | } 22 | 23 | #[test] 24 | fn issue_70() { 25 | assert_eq!( 26 | Enum::decl(), 27 | "type Enum = { \"A\": { [key in string]?: string } } | { \"B\": { [key in string]?: string } };" 28 | ); 29 | assert_eq!( 30 | Struct::decl(), 31 | "type Struct = { a: { [key in string]?: string }, b: { [key in string]?: string }, };" 32 | ); 33 | } 34 | 35 | #[derive(TS)] 36 | #[ts(export, export_to = "issue_70/")] 37 | struct GenericType { 38 | foo: T, 39 | bar: U, 40 | } 41 | 42 | type GenericAlias = GenericType<(A, String), Vec<(B, i32)>>; 43 | 44 | #[derive(TS)] 45 | #[ts(export, export_to = "issue_70/")] 46 | struct Container { 47 | a: GenericAlias, Vec>, 48 | b: GenericAlias, 49 | } 50 | 51 | #[derive(TS)] 52 | #[ts(export, export_to = "issue_70/")] 53 | struct GenericContainer { 54 | a: GenericAlias, 55 | b: GenericAlias, 56 | c: GenericAlias>, 57 | } 58 | 59 | #[test] 60 | fn generic() { 61 | assert_eq!( 62 | Container::decl(), 63 | "type Container = { \ 64 | a: GenericType<[Array, string], Array<[Array, number]>>, \ 65 | b: GenericType<[string, string], Array<[string, number]>>, \ 66 | };" 67 | ); 68 | 69 | assert_eq!( 70 | GenericContainer::<(), ()>::decl(), 71 | "type GenericContainer = { \ 72 | a: GenericType<[string, string], Array<[string, number]>>, \ 73 | b: GenericType<[A, string], Array<[B, number]>>, \ 74 | c: GenericType<[A, string], Array<[GenericType<[A, string], Array<[B, number]>>, number]>>, \ 75 | };" 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/issue_80.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use ts_rs::TS; 3 | 4 | #[derive(TS, Serialize)] 5 | #[ts(export, export_to = "issue_80/")] 6 | pub enum SomeTypeList { 7 | Value1 { 8 | #[serde(skip)] 9 | #[ts(skip)] 10 | skip_this: String, 11 | }, 12 | Value2, 13 | } 14 | 15 | #[test] 16 | fn issue_80() { 17 | let ty = SomeTypeList::inline(); 18 | assert_eq!(ty, r#"{ "Value1": { } } | "Value2""#); 19 | } 20 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/leading_colon.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use ::ts_rs::TS; 4 | 5 | mod ts_rs {} 6 | 7 | #[derive(TS)] 8 | struct Foo { 9 | x: u32, 10 | } 11 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/lifetimes.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "lifetimes/")] 7 | struct S<'a> { 8 | s: &'a str, 9 | } 10 | 11 | #[derive(TS)] 12 | #[ts(export, export_to = "lifetimes/")] 13 | struct B<'a, T: 'a> { 14 | a: &'a T, 15 | } 16 | 17 | #[derive(TS)] 18 | #[ts(export, export_to = "lifetimes/")] 19 | struct A<'a> { 20 | a: &'a &'a &'a Vec, //Multiple References 21 | b: &'a Vec>, //Nesting 22 | c: &'a std::collections::HashMap, //Multiple type args 23 | } 24 | 25 | #[test] 26 | fn contains_borrow() { 27 | assert_eq!(S::decl(), "type S = { s: string, };") 28 | } 29 | 30 | #[test] 31 | fn contains_borrow_type_args() { 32 | assert_eq!( 33 | A::decl(), 34 | "type A = { a: Array, b: Array>, c: { [key in string]?: boolean }, };" 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/list.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use ts_rs::TS; 3 | 4 | #[derive(TS)] 5 | #[ts(export, export_to = "list/")] 6 | struct List { 7 | data: Option>, 8 | } 9 | 10 | #[test] 11 | fn list() { 12 | assert_eq!(List::decl(), "type List = { data: Array | null, };"); 13 | } 14 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused)] 2 | 3 | mod arrays; 4 | mod bound; 5 | mod bson; 6 | mod chrono; 7 | mod complex_flattened_type; 8 | mod concrete_generic; 9 | mod docs; 10 | mod enum_flattening; 11 | mod enum_flattening_nested; 12 | mod enum_struct_rename_all; 13 | mod enum_variant_annotation; 14 | mod export_manually; 15 | mod export_to; 16 | mod field_rename; 17 | mod flatten; 18 | mod generic_fields; 19 | mod generic_without_import; 20 | mod generics; 21 | mod generics_flatten; 22 | mod hashmap; 23 | mod hashset; 24 | mod impl_primitive; 25 | mod imports; 26 | mod indexmap; 27 | mod infer_as; 28 | mod issue_168; 29 | mod issue_232; 30 | mod issue_308; 31 | mod issue_317; 32 | mod issue_338; 33 | mod issue_397; 34 | mod issue_415; 35 | mod issue_70; 36 | mod issue_80; 37 | mod leading_colon; 38 | mod lifetimes; 39 | mod list; 40 | mod merge_same_file_imports; 41 | mod nested; 42 | mod optional_field; 43 | mod path_bug; 44 | mod ranges; 45 | mod raw_idents; 46 | mod recursion_limit; 47 | mod references; 48 | mod same_file_export; 49 | mod self_referential; 50 | mod semver; 51 | mod serde_json; 52 | mod serde_skip_serializing; 53 | mod serde_skip_with_default; 54 | mod serde_with; 55 | mod simple; 56 | mod skip; 57 | mod slices; 58 | mod struct_rename; 59 | mod struct_tag; 60 | mod tokio; 61 | mod top_level_type_as; 62 | mod top_level_type_override; 63 | mod tuple; 64 | mod type_as; 65 | mod type_override; 66 | mod union; 67 | mod union_named_serde_skip; 68 | mod union_rename; 69 | mod union_serde; 70 | mod union_unnamed_serde_skip; 71 | mod union_with_data; 72 | mod union_with_internal_tag; 73 | mod unit; 74 | mod r#unsized; 75 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/merge_same_file_imports.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use serde::de::Expected; 4 | use ts_rs::TS; 5 | 6 | #[derive(TS)] 7 | #[ts(export_to = "merge_same_file_imports/a.ts")] 8 | pub struct EditProfile { 9 | pub name: Option, 10 | 11 | pub game_version: Option, 12 | pub loader: Option, 13 | 14 | pub hooks: Option, 15 | pub extra: Extra, 16 | } 17 | 18 | #[derive(TS)] 19 | #[ts(rename_all = "lowercase", export_to = "merge_same_file_imports/b.ts")] 20 | pub enum ModLoader { 21 | Vanilla, 22 | Forge, 23 | Fabric, 24 | Quilt, 25 | NeoForge, 26 | } 27 | 28 | #[derive(TS)] 29 | #[ts(export_to = "merge_same_file_imports/b.ts")] 30 | pub struct Hooks { 31 | pub pre_launch: Option, 32 | pub wrapper: Option, 33 | pub post_exit: Option, 34 | } 35 | 36 | #[derive(TS)] 37 | #[ts(export_to = "merge_same_file_imports/c.ts")] 38 | pub struct Extra { 39 | pub foo: i32, 40 | } 41 | 42 | #[test] 43 | fn merge_same_file_imports() { 44 | EditProfile::export_all().unwrap(); 45 | let text = std::fs::read_to_string(EditProfile::default_output_path().unwrap()).unwrap(); 46 | 47 | let extension = if cfg!(feature = "import-esm") { 48 | ".js" 49 | } else { 50 | "" 51 | }; 52 | 53 | let mut expected = String::with_capacity(text.len()); 54 | writeln!(expected, "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.").unwrap(); 55 | writeln!( 56 | expected, 57 | r#"import type {{ Hooks, ModLoader }} from "./b{extension}";"# 58 | ) 59 | .unwrap(); 60 | writeln!( 61 | expected, 62 | r#"import type {{ Extra }} from "./c{extension}";"# 63 | ) 64 | .unwrap(); 65 | writeln!(expected); 66 | 67 | if cfg!(feature = "format") { 68 | writeln!(expected, "export type EditProfile = {{").unwrap(); 69 | writeln!(expected, " name: string | null;").unwrap(); 70 | writeln!(expected, " game_version: string | null;").unwrap(); 71 | writeln!(expected, " loader: ModLoader | null;").unwrap(); 72 | writeln!(expected, " hooks: Hooks | null;").unwrap(); 73 | writeln!(expected, " extra: Extra;").unwrap(); 74 | writeln!(expected, "}};").unwrap(); 75 | } else { 76 | writeln!(expected, "export {}", EditProfile::decl()).unwrap(); 77 | } 78 | 79 | assert_eq!(text, expected) 80 | } 81 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/nested.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{cell::Cell, rc::Rc, sync::Arc}; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "nested/")] 9 | struct A { 10 | x1: Arc, 11 | y1: Cell, 12 | } 13 | 14 | #[derive(TS)] 15 | #[ts(export, export_to = "nested/")] 16 | struct B { 17 | a1: Box, 18 | #[ts(inline)] 19 | a2: A, 20 | } 21 | 22 | #[derive(TS)] 23 | #[ts(export, export_to = "nested/")] 24 | struct C { 25 | b1: Rc, 26 | #[ts(inline)] 27 | b2: B, 28 | } 29 | 30 | #[test] 31 | fn test_nested() { 32 | assert_eq!( 33 | C::inline(), 34 | "{ b1: B, b2: { a1: A, a2: { x1: number, y1: number, }, }, }" 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/optional_field.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use serde::Serialize; 4 | use ts_rs::TS; 5 | 6 | #[derive(Serialize, TS)] 7 | #[ts(export, export_to = "optional_field/")] 8 | struct OptionalInStruct { 9 | #[ts(optional)] 10 | a: Option, 11 | #[ts(optional = nullable)] 12 | b: Option, 13 | c: Option, 14 | } 15 | 16 | #[test] 17 | fn in_struct() { 18 | let a = "a?: number"; 19 | let b = "b?: number | null"; 20 | let c = "c: number | null"; 21 | assert_eq!(OptionalInStruct::inline(), format!("{{ {a}, {b}, {c}, }}")); 22 | } 23 | 24 | #[derive(Serialize, TS)] 25 | #[ts(export, export_to = "optional_field/")] 26 | enum OptionalInEnum { 27 | A { 28 | #[ts(optional)] 29 | a: Option, 30 | }, 31 | B { 32 | b: Option, 33 | }, 34 | } 35 | 36 | #[test] 37 | fn in_enum() { 38 | assert_eq!( 39 | OptionalInEnum::inline(), 40 | r#"{ "A": { a?: number, } } | { "B": { b: string | null, } }"# 41 | ); 42 | } 43 | 44 | #[derive(Serialize, TS)] 45 | #[ts(export, export_to = "optional_field/")] 46 | struct OptionalFlatten { 47 | #[ts(optional)] 48 | a: Option, 49 | #[ts(optional = nullable)] 50 | b: Option, 51 | c: Option, 52 | } 53 | 54 | #[derive(Serialize, TS)] 55 | #[ts(export, export_to = "optional_field/")] 56 | struct Flatten { 57 | #[ts(flatten)] 58 | x: OptionalFlatten, 59 | } 60 | 61 | #[test] 62 | fn flatten() { 63 | assert_eq!(Flatten::inline(), OptionalFlatten::inline()); 64 | } 65 | 66 | #[derive(Serialize, TS)] 67 | #[ts(export, export_to = "optional_field/")] 68 | struct OptionalInline { 69 | #[ts(optional)] 70 | a: Option, 71 | #[ts(optional = nullable)] 72 | b: Option, 73 | c: Option, 74 | } 75 | 76 | #[derive(Serialize, TS)] 77 | #[ts(export, export_to = "optional_field/")] 78 | struct Inline { 79 | #[ts(inline)] 80 | x: OptionalInline, 81 | } 82 | 83 | #[test] 84 | fn inline() { 85 | let a = "a?: number"; 86 | let b = "b?: number | null"; 87 | let c = "c: number | null"; 88 | assert_eq!(Inline::inline(), format!("{{ x: {{ {a}, {b}, {c}, }}, }}")); 89 | } 90 | 91 | type Foo = Option; 92 | type Bar = Option; 93 | 94 | #[derive(TS)] 95 | #[ts(export, export_to = "optional_field/", optional_fields)] 96 | struct OptionalStruct { 97 | a: Option, 98 | b: Option, 99 | 100 | #[ts(optional = nullable)] 101 | c: Option, 102 | 103 | d: i32, 104 | 105 | e: Foo, 106 | f: Bar, 107 | 108 | #[ts(type = "string")] 109 | g: Option, 110 | 111 | #[ts(as = "String")] 112 | h: Option, 113 | } 114 | 115 | #[test] 116 | fn struct_optional() { 117 | assert_eq!( 118 | OptionalStruct::inline(), 119 | format!( 120 | "{{ a?: number, b?: number, c?: number | null, d: number, e?: number, f?: number, g: string, h: string, }}" 121 | ) 122 | ) 123 | } 124 | 125 | #[derive(TS)] 126 | #[ts(export, export_to = "optional_field/", optional_fields = nullable)] 127 | struct NullableStruct { 128 | a: Option, 129 | b: Option, 130 | 131 | #[ts(optional = nullable)] 132 | c: Option, 133 | 134 | d: i32, 135 | 136 | e: Foo, 137 | f: Bar, 138 | 139 | #[ts(type = "string")] 140 | g: Option, 141 | 142 | #[ts(as = "String")] 143 | h: Option, 144 | } 145 | 146 | #[test] 147 | fn struct_nullable() { 148 | assert_eq!( 149 | NullableStruct::inline(), 150 | format!( 151 | "{{ a?: number | null, b?: number | null, c?: number | null, d: number, e?: number | null, f?: number | null, g: string, h: string, }}" 152 | ) 153 | ) 154 | } 155 | 156 | #[derive(Serialize, TS)] 157 | #[ts(export, export_to = "optional_field/")] 158 | struct OptionalInTuple( 159 | Option, 160 | #[ts(optional)] Option, 161 | #[ts(optional = nullable)] Option, 162 | ); 163 | 164 | #[test] 165 | fn in_tuple() { 166 | assert_eq!( 167 | OptionalInTuple::inline(), 168 | format!("[number | null, (number)?, (number | null)?]") 169 | ); 170 | } 171 | 172 | #[derive(Serialize, TS)] 173 | #[ts(export, export_to = "optional_field/")] 174 | #[ts(optional_fields)] 175 | struct OptionalTuple( 176 | i32, 177 | #[ts(type = "string")] Option, 178 | #[ts(as = "String")] Option, 179 | Option, 180 | #[ts(optional)] Option, 181 | #[ts(optional = nullable)] Option, 182 | ); 183 | 184 | #[test] 185 | fn tuple_optional() { 186 | assert_eq!( 187 | OptionalTuple::inline(), 188 | "[number, string, string, (number)?, (number)?, (number | null)?]" 189 | ); 190 | } 191 | 192 | #[derive(Serialize, TS)] 193 | #[ts(export, export_to = "optional_field/")] 194 | #[ts(optional_fields = nullable)] 195 | struct NullableTuple( 196 | i32, 197 | #[ts(type = "string")] Option, 198 | #[ts(as = "String")] Option, 199 | Option, 200 | #[ts(optional)] Option, 201 | #[ts(optional = nullable)] Option, 202 | ); 203 | 204 | #[test] 205 | fn tuple_nullable() { 206 | assert_eq!( 207 | NullableTuple::inline(), 208 | "[number, string, string, (number | null)?, (number)?, (number | null)?]" 209 | ); 210 | } 211 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/path_bug.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use ts_rs::TS; 3 | 4 | #[derive(TS)] 5 | #[ts(export, export_to = "path_bug/aaa/")] 6 | struct Foo { 7 | bar: Bar, 8 | } 9 | 10 | #[derive(TS)] 11 | #[ts(export_to = "../bindings/path_bug/")] 12 | struct Bar { 13 | i: i32, 14 | } 15 | 16 | #[test] 17 | fn path_bug() { 18 | export_bindings_foo(); 19 | 20 | assert!(Foo::default_output_path().unwrap().is_file()); 21 | assert!(Bar::default_output_path().unwrap().is_file()); 22 | } 23 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/ranges.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{ 4 | collections::BTreeSet, 5 | ops::{Range, RangeInclusive}, 6 | }; 7 | 8 | use ts_rs::{Dependency, TS}; 9 | 10 | #[derive(TS)] 11 | #[ts(export, export_to = "ranges/")] 12 | struct Inner(i32); 13 | 14 | #[derive(TS)] 15 | #[ts(export, export_to = "ranges/")] 16 | struct RangeTest { 17 | a: Range, 18 | b: Range<&'static str>, 19 | c: Range>, 20 | d: RangeInclusive, 21 | e: Range, 22 | } 23 | 24 | #[test] 25 | fn range() { 26 | assert_eq!( 27 | RangeTest::decl(), 28 | "type RangeTest = { \ 29 | a: { start: number, end: number, }, \ 30 | b: { start: string, end: string, }, \ 31 | c: { \ 32 | start: { start: number, end: number, }, \ 33 | end: { start: number, end: number, }, \ 34 | }, \ 35 | d: { start: number, end: number, }, \ 36 | e: { start: Inner, end: Inner, }, \ 37 | };" 38 | ); 39 | assert_eq!( 40 | RangeTest::dependencies() 41 | .into_iter() 42 | .collect::>() 43 | .into_iter() 44 | .collect::>(), 45 | vec![Dependency::from_ty::().unwrap(),] 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/raw_idents.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "raw_idents/")] 7 | struct r#struct { 8 | r#type: i32, 9 | r#use: i32, 10 | r#struct: i32, 11 | r#let: i32, 12 | r#enum: i32, 13 | } 14 | 15 | #[test] 16 | fn raw_idents() { 17 | let out = ::decl(); 18 | assert_eq!( 19 | out, 20 | "type struct = { type: number, use: number, struct: number, let: number, enum: number, };" 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/recursion_limit.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | 3 | use ts_rs::{TypeVisitor, TS}; 4 | 5 | #[rustfmt::skip] 6 | #[allow(clippy::all)] 7 | #[derive(Debug, ts_rs::TS)] 8 | #[ts(export, export_to = "very_big_types/")] 9 | pub enum Iso4217CurrencyCode { 10 | AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, 11 | BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, DKK, 12 | DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GGP, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, 13 | HRK, HTG, HUF, IDR, ILS, IMP, INR, IQD, IRR, ISK, JEP, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, 14 | KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, 15 | MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, 16 | QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLL, SOS, SPL, SRD, STN, SVC, SYP, 17 | SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TVD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VEF, VND, VUV, 18 | WST, XAF, XCD, XDR, XOF, XPF, YER, ZAR, ZMW, ZWD, 19 | } 20 | 21 | #[rustfmt::skip] 22 | #[derive(Debug, ts_rs::TS)] 23 | #[ts(export, export_to = "very_big_types/")] 24 | pub enum VeryBigEnum { 25 | V001(String), V002(String), V003(String), V004(String), V005(String), V006(String), V007(String), 26 | V008(String), V009(String), V010(String), V011(String), V012(String), V013(String), V014(String), 27 | V015(String), V016(String), V017(String), V018(String), V019(String), V020(String), V021(String), 28 | V022(String), V023(String), V024(String), V025(String), V026(String), V027(String), V028(String), 29 | V029(String), V030(String), V031(String), V032(String), V033(String), V034(String), V035(String), 30 | V036(String), V037(String), V038(String), V039(String), V040(String), V041(String), V042(String), 31 | V043(String), V044(String), V045(String), V046(String), V047(String), V048(String), V049(String), 32 | V050(String), V051(String), V052(String), V053(String), V054(String), V055(String), V056(String), 33 | V057(String), V058(String), V059(String), V060(String), V061(String), V062(String), V063(String), 34 | V064(String), V065(String), V066(String), V067(String), V068(String), V069(String), V070(String), 35 | V071(String), V072(String), V073(String), V074(String), V075(String), V076(String), V077(String), 36 | V078(String), V079(String), V080(String), V081(String), V082(String), V083(String), V084(String), 37 | V085(String), V086(String), V087(String), V088(String), V089(String), V090(String), V091(String), 38 | V092(String), V093(String), V094(String), V095(String), V096(String), V097(String), V098(String), 39 | V099(String), V100(String), V101(String), V102(String), V103(String), V104(String), V105(String), 40 | V106(String), V107(String), V108(String), V109(String), V110(String), V111(String), V112(String), 41 | V113(String), V114(String), V115(String), V116(String), V117(String), V118(String), V119(String), 42 | V120(String), V121(String), V122(String), V123(String), V124(String), V125(String), V126(String), 43 | V127(String), V128(String), V129(String), V130(String), V131(String), V132(String), V133(String), 44 | V134(String), V135(String), V136(String), V137(String), V138(String), V139(String), V140(String), 45 | V141(String), V142(String), V143(String), V144(String), V145(String), V146(String), V147(String), 46 | V148(String), V149(String), V150(String), V151(String), V152(String), V153(String), V154(String), 47 | V155(String), V156(String), V157(String), V158(String), V159(String), V160(String), V161(String), 48 | V162(String), V163(String), V164(String), V165(String), V166(String), V167(String), V168(String), 49 | V169(String), V170(String), V171(String), V172(String), V173(String), V174(String), V175(String), 50 | V176(String), V177(String), V178(String), V179(String), V180(String), V181(String), V182(String), 51 | V183(String), V184(String), V185(String), V186(String), V187(String), V188(String), V189(String), 52 | V190(String), V191(String), V192(String), V193(String), V194(String), V195(String), V196(String), 53 | V197(String), V198(String), V199(String), V200(String), V201(String), V202(String), V203(String), 54 | V204(String), V205(String), V206(String), V207(String), V208(String), V209(String), V210(String), 55 | V211(String), V212(String), V213(String), V214(String), V215(String), V216(String), V217(String), 56 | V218(String), V219(String), V220(String), V221(String), V222(String), V223(String), V224(String), 57 | V225(String), V226(String), V227(String), V228(String), V229(String), V230(String), V231(String), 58 | V232(String), V233(String), V234(String), V235(String), V236(String), V237(String), V238(String), 59 | V239(String), V240(String), V241(String), V242(String), V243(String), V244(String), V245(String), 60 | V246(String), V247(String), V248(String), V249(String), V250(String), V251(String), V252(String), 61 | V253(String), V254(String), V255(String), V256(String), 62 | } 63 | 64 | #[test] 65 | fn very_big_enum() { 66 | struct Visitor(bool); 67 | 68 | impl TypeVisitor for Visitor { 69 | fn visit(&mut self) { 70 | assert!(!self.0, "there must only be one dependency"); 71 | assert_eq!(TypeId::of::(), TypeId::of::()); 72 | self.0 = true; 73 | } 74 | } 75 | 76 | let mut visitor = Visitor(false); 77 | VeryBigEnum::visit_dependencies(&mut visitor); 78 | 79 | assert!(visitor.0, "there must be at least one dependency"); 80 | } 81 | 82 | macro_rules! generate_types { 83 | ($a:ident, $b:ident $($t:tt)*) => { 84 | #[derive(TS)] 85 | #[ts(export, export_to = "very_big_types/")] 86 | struct $a($b); 87 | generate_types!($b $($t)*); 88 | }; 89 | ($a:ident) => { 90 | #[derive(TS)] 91 | #[ts(export, export_to = "very_big_types/")] 92 | struct $a; 93 | } 94 | } 95 | 96 | // This generates 97 | // `#[derive(TS)] struct T000(T001)` 98 | // `#[derive(TS)] struct T001(T002)` 99 | // ... 100 | // `#[derive(TS)] struct T082(T083)` 101 | // `#[derive(TS)] struct T083;` 102 | generate_types!( 103 | T000, T001, T002, T003, T004, T005, T006, T007, T008, T009, T010, T011, T012, T013, T014, T015, 104 | T016, T017, T018, T019, T020, T021, T022, T023, T024, T025, T026, T027, T028, T029, T030, T031, 105 | T032, T033, T034, T035, T036, T037, T038, T039, T040, T041, T042, T043, T044, T045, T046, T047, 106 | T048, T049, T050, T051, T052, T053, T054, T055, T056, T057, T058, T059, T060, T061, T062, T063, 107 | T064, T065, T066, T067, T068, T069, T070, T071, T072, T073, T074, T075, T076, T077, T078, T079, 108 | T080, T081, T082, T083 109 | ); 110 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/references.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "references/")] 7 | struct FullOfRefs<'a> { 8 | str_slice: &'a str, 9 | ref_slice: &'a [&'a str], 10 | num_ref: &'a i32, 11 | } 12 | 13 | #[test] 14 | fn references() { 15 | assert_eq!( 16 | FullOfRefs::inline(), 17 | "{ str_slice: string, ref_slice: Array, num_ref: number, }" 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/same_file_export.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | #[derive(TS)] 4 | #[ts(export, export_to = "same_file_export/")] 5 | struct DepA { 6 | foo: i32, 7 | } 8 | 9 | #[derive(TS)] 10 | #[ts(export, export_to = "same_file_export/")] 11 | struct DepB { 12 | foo: i32, 13 | } 14 | 15 | #[derive(TS)] 16 | #[ts(export, export_to = "same_file_export/types.ts")] 17 | struct A { 18 | foo: DepA, 19 | } 20 | 21 | #[derive(TS)] 22 | #[ts(export, export_to = "same_file_export/types.ts")] 23 | struct B { 24 | foo: DepB, 25 | } 26 | 27 | #[derive(TS)] 28 | #[ts(export, export_to = "same_file_export/types.ts")] 29 | struct C { 30 | foo: DepA, 31 | bar: DepB, 32 | biz: B, 33 | } 34 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/self_referential.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use std::{collections::HashMap, sync::Arc}; 3 | 4 | #[cfg(feature = "serde-compat")] 5 | use serde::Serialize; 6 | use ts_rs::TS; 7 | 8 | #[derive(TS)] 9 | #[ts(export, export_to = "self_referential/")] 10 | struct HasT { 11 | t: &'static T<'static>, 12 | } 13 | 14 | #[derive(TS)] 15 | #[ts(export, export_to = "self_referential/")] 16 | struct T<'a> { 17 | t_box: Box>, 18 | self_box: Box, 19 | 20 | t_ref: &'a T<'a>, 21 | self_ref: &'a Self, 22 | 23 | t_arc: Arc>, 24 | self_arc: Arc, 25 | 26 | #[ts(inline)] 27 | has_t: HasT, 28 | } 29 | 30 | #[test] 31 | fn named() { 32 | assert_eq!( 33 | T::decl(), 34 | "type T = { \ 35 | t_box: T, \ 36 | self_box: T, \ 37 | t_ref: T, \ 38 | self_ref: T, \ 39 | t_arc: T, \ 40 | self_arc: T, \ 41 | has_t: { t: T, }, \ 42 | };" 43 | ); 44 | } 45 | 46 | #[derive(TS)] 47 | #[ts(export, export_to = "self_referential/", rename = "E")] 48 | enum ExternallyTagged { 49 | A(Box), 50 | B(&'static ExternallyTagged), 51 | C(Box), 52 | D(&'static Self), 53 | E( 54 | Box, 55 | Box, 56 | &'static ExternallyTagged, 57 | &'static Self, 58 | ), 59 | F { 60 | a: Box, 61 | b: &'static ExternallyTagged, 62 | c: HashMap, 63 | d: Option>, 64 | #[ts(optional = nullable)] 65 | e: Option>, 66 | #[ts(optional)] 67 | f: Option>, 68 | }, 69 | 70 | G( 71 | Vec, 72 | [&'static ExternallyTagged; 1024], 73 | HashMap, 74 | ), 75 | } 76 | 77 | #[test] 78 | fn enum_externally_tagged() { 79 | assert_eq!( 80 | ExternallyTagged::decl(), 81 | "type E = { \"A\": E } | \ 82 | { \"B\": E } | \ 83 | { \"C\": E } | \ 84 | { \"D\": E } | \ 85 | { \"E\": [E, E, E, E] } | \ 86 | { \"F\": { a: E, b: E, c: { [key in string]?: E }, d: E | null, e?: E | null, f?: E, } } | \ 87 | { \"G\": [Array, Array, { [key in string]?: E }] };" 88 | ); 89 | } 90 | 91 | #[derive(TS)] 92 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 93 | #[ts(rename = "I")] 94 | #[cfg_attr(feature = "serde-compat", serde(tag = "tag"))] 95 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "tag"))] 96 | enum InternallyTagged { 97 | A(Box), 98 | B(&'static InternallyTagged), 99 | C(Box), 100 | D(&'static Self), 101 | E(Vec), 102 | F { 103 | a: Box, 104 | b: &'static InternallyTagged, 105 | c: HashMap, 106 | d: Option<&'static InternallyTagged>, 107 | #[ts(optional = nullable)] 108 | e: Option<&'static InternallyTagged>, 109 | #[ts(optional)] 110 | f: Option<&'static InternallyTagged>, 111 | }, 112 | } 113 | 114 | // NOTE: The generated type is actually not valid TS here, since the indirections rust enforces for recursive types 115 | // gets lost during the translation to TypeScript (e.g "Box" => "T"). 116 | #[test] 117 | fn enum_internally_tagged() { 118 | assert_eq!( 119 | InternallyTagged::decl(), 120 | "type I = { \"tag\": \"A\" } & I | \ 121 | { \"tag\": \"B\" } & I | \ 122 | { \"tag\": \"C\" } & I | \ 123 | { \"tag\": \"D\" } & I | \ 124 | { \"tag\": \"E\" } & Array | \ 125 | { \"tag\": \"F\", a: I, b: I, c: { [key in I]?: I }, d: I | null, e?: I | null, f?: I, };" 126 | ); 127 | } 128 | 129 | #[derive(TS)] 130 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 131 | #[ts(export, export_to = "self_referential/", rename = "A")] 132 | #[cfg_attr(feature = "serde-compat", serde(tag = "tag", content = "content"))] 133 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "tag", content = "content"))] 134 | enum AdjacentlyTagged { 135 | A(Box), 136 | B(&'static AdjacentlyTagged), 137 | C(Box), 138 | D(&'static Self), 139 | E(Vec), 140 | F { 141 | a: Box, 142 | b: &'static AdjacentlyTagged, 143 | c: HashMap, 144 | d: Option<&'static AdjacentlyTagged>, 145 | #[ts(optional = nullable)] 146 | e: Option<&'static AdjacentlyTagged>, 147 | #[ts(optional)] 148 | f: Option<&'static AdjacentlyTagged>, 149 | }, 150 | G( 151 | Vec, 152 | [&'static AdjacentlyTagged; 4], 153 | HashMap, 154 | ), 155 | } 156 | 157 | // NOTE: The generated type is actually not valid TS here, since the indirections rust enforces for recursive types 158 | // gets lost during the translation to TypeScript (e.g "Box" => "T"). 159 | #[test] 160 | fn enum_adjacently_tagged() { 161 | assert_eq!( 162 | AdjacentlyTagged::decl(), 163 | "type A = { \"tag\": \"A\", \"content\": A } | \ 164 | { \"tag\": \"B\", \"content\": A } | \ 165 | { \"tag\": \"C\", \"content\": A } | \ 166 | { \"tag\": \"D\", \"content\": A } | \ 167 | { \"tag\": \"E\", \"content\": Array } | \ 168 | { \ 169 | \"tag\": \"F\", \ 170 | \"content\": { \ 171 | a: A, \ 172 | b: A, \ 173 | c: { [key in string]?: A }, \ 174 | d: A | null, \ 175 | e?: A | null, \ 176 | f?: A, \ 177 | } \ 178 | } | \ 179 | { \ 180 | \"tag\": \"G\", \ 181 | \"content\": [\ 182 | Array, \ 183 | [A, A, A, A], \ 184 | { [key in string]?: A }\ 185 | ] \ 186 | };" 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/semver.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![cfg(feature = "semver-impl")] 3 | 4 | use semver::Version; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "semver/")] 9 | struct Semver { 10 | version: Version, 11 | } 12 | 13 | #[test] 14 | fn semver() { 15 | assert_eq!(Semver::decl(), "type Semver = { version: string, };") 16 | } 17 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/serde_json.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde_json")] 2 | #![allow(unused)] 3 | 4 | use ts_rs::TS; 5 | 6 | #[derive(TS)] 7 | #[ts(export, export_to = "serde_json_impl/")] 8 | struct UsingSerdeJson { 9 | num: serde_json::Number, 10 | map1: serde_json::Map, 11 | map2: serde_json::Map, 12 | map3: serde_json::Map>, 13 | map4: serde_json::Map, 14 | map5: serde_json::Map, 15 | any: serde_json::Value, 16 | } 17 | 18 | #[test] 19 | fn using_serde_json() { 20 | assert_eq!(serde_json::Number::inline(), "number"); 21 | assert_eq!( 22 | serde_json::Map::::inline(), 23 | "{ [key in string]?: number }" 24 | ); 25 | assert_eq!( 26 | serde_json::Value::decl(), 27 | "type JsonValue = number | string | boolean | Array | { [key in string]?: JsonValue } | null;", 28 | ); 29 | 30 | assert_eq!( 31 | UsingSerdeJson::decl(), 32 | "type UsingSerdeJson = { \ 33 | num: number, \ 34 | map1: { [key in string]?: number }, \ 35 | map2: { [key in string]?: UsingSerdeJson }, \ 36 | map3: { [key in string]?: { [key in string]?: number } }, \ 37 | map4: { [key in string]?: number }, \ 38 | map5: { [key in string]?: JsonValue }, \ 39 | any: JsonValue, \ 40 | };" 41 | ) 42 | } 43 | 44 | #[derive(TS)] 45 | #[ts(export, export_to = "serde_json_impl/")] 46 | struct InlinedValue { 47 | #[ts(inline)] 48 | any: serde_json::Value, 49 | } 50 | 51 | #[test] 52 | fn inlined_value() { 53 | assert_eq!( 54 | InlinedValue::decl(), 55 | "type InlinedValue = { \ 56 | any: number | string | boolean | Array | { [key in string]?: JsonValue } | null, \ 57 | };" 58 | ); 59 | } 60 | 61 | #[derive(TS)] 62 | #[ts(export, export_to = "serde_json_impl/")] 63 | struct Simple { 64 | json: serde_json::Value, 65 | } 66 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/serde_skip_serializing.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde-compat")] 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use ts_rs::TS; 5 | 6 | // A field annotated with both `#[serde(skip_serializing(_if))]` and `#[serde(default)]` is treated 7 | // like `#[ts(optional = nullable)]` (except no errors if the type is not `Option`) 8 | #[derive(Debug, Clone, Deserialize, Serialize, TS)] 9 | #[ts(export, export_to = "serde_skip_serializing/")] 10 | pub struct Named { 11 | // Serialization produces: `number | undefined | null(1)` 12 | // (1): We'd know `null` is never produced if we checked the predicate 13 | // Deserialization accepts: `number | null` 14 | // (2): `undefined` is also accepted, but only with serde_json 15 | // 16 | // There is no type we can choose which accepts every possible value in both directions. 17 | // Therefore, we stick with the default, ignoring the annotations: `a: number | null`. 18 | // 19 | // When TS receives a value from Rust, users might get `undefined`, causing a runtime error. 20 | // When TS sends a value to Rust, the type is guaranteed to be correct. 21 | // => Possible runtime error in TS 22 | // 23 | // If we instead generated `a?: number`: 24 | // When TS receives a value from Rust, a runtime error may occur if the value is `null` (1) 25 | // When TS sends a value to Rust, a runtime error may occur if the value is `undefined` (2) 26 | // => Possible runtime errors in TS and Rust 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | a: Option, 29 | 30 | // Serialization produces: `boolean | undefined` 31 | // Deserialization accepts: `boolean | undefined` 32 | // Most general binding: `b?: boolean` 33 | #[serde(skip_serializing_if = "std::ops::Not::not", default)] 34 | b: bool, 35 | 36 | // Serialization produces: `number | undefined | null(1)` 37 | // Deserialization accepts: `number | undefined | null` 38 | // Most general binding: `c?: number | null` 39 | // 40 | // (1): If we could ensure that the predicate always skips for `None`, then we'd know for sure 41 | // that serialization never produces `null`. 42 | // Then, we'd have the choice between `c?: number` and `c?: number | null`. 43 | // The first would incorrectly prevent users from deserializing `null`, and 44 | // the second wouldn't tell users that `null` is never serialized 45 | #[serde(skip_serializing_if = "Option::is_none", default)] 46 | c: Option, 47 | 48 | // Serialization produces: ` undefined ` 49 | // Deserialization accepts: `number | undefined | null` 50 | // Most general binding: `d?: number | null` 51 | #[serde(skip_serializing, default)] 52 | d: Option, 53 | 54 | // Same as above, but explicitly overridden using `#[ts(optional = false)]` 55 | #[serde(skip_serializing, default)] 56 | #[ts(optional = false)] 57 | e: Option, 58 | 59 | // Same as above, but explicitly overridden using `#[ts(optional)]`. 60 | #[serde(skip_serializing, default)] 61 | #[ts(optional)] 62 | f: Option, 63 | } 64 | 65 | #[test] 66 | fn named() { 67 | let a = "a: number | null"; 68 | let b = "b?: boolean"; 69 | let c = "c?: number | null"; 70 | let d = "d?: number | null"; 71 | let e = "e: number | null"; 72 | let f = "f?: number"; 73 | 74 | assert_eq!( 75 | Named::decl(), 76 | format!("type Named = {{ {a}, {b}, {c}, {d}, {e}, {f}, }};") 77 | ); 78 | } 79 | 80 | #[derive(Debug, Clone, Deserialize, Serialize, TS)] 81 | #[ts(export, export_to = "serde_skip_serializing/")] 82 | pub struct Tuple( 83 | Option, 84 | #[ts(optional)] Option, 85 | #[serde(skip_serializing, default)] Option, 86 | ); 87 | 88 | #[test] 89 | fn tuple() { 90 | assert_eq!( 91 | Tuple::decl(), 92 | "type Tuple = [number | null, (number)?, (number | null)?];" 93 | ); 94 | } 95 | 96 | #[derive(Debug, Clone, Deserialize, Serialize, TS)] 97 | #[ts(export, export_to = "serde_skip_serializing/")] 98 | #[ts(optional_fields = false)] 99 | pub struct Overrides { 100 | #[serde(skip_serializing, default)] 101 | x: Option, 102 | y: Option, 103 | #[ts(optional)] 104 | z: Option, 105 | } 106 | 107 | #[test] 108 | fn overrides() { 109 | let x = "x: number | null"; // same as without any attributes, since it's disabled at the struct level 110 | let y = "y: number | null"; // default type for an option 111 | let z = "z?: number"; // re-enabled for this field 112 | assert_eq!( 113 | Overrides::decl(), 114 | format!("type Overrides = {{ {x}, {y}, {z}, }};") 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/serde_skip_with_default.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde-compat")] 2 | #![allow(dead_code)] 3 | 4 | // from issue #107. This does now no longer generate a warning. 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use ts_rs::TS; 8 | 9 | fn default_http_version() -> String { 10 | "2".to_owned() 11 | } 12 | 13 | #[derive(Debug, Clone, Deserialize, Serialize, TS)] 14 | #[ts(export, export_to = "serde_skip_with_default/")] 15 | pub struct Foobar { 16 | // #[ts(skip)] 17 | #[serde(skip, default = "default_http_version")] 18 | pub http_version: String, 19 | pub something_else: i32, 20 | } 21 | 22 | #[test] 23 | fn serde_skip_with_default() { 24 | assert_eq!(Foobar::decl(), "type Foobar = { something_else: number, };"); 25 | } 26 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/serde_with.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused, dead_code, clippy::disallowed_names)] 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use ts_rs::TS; 5 | 6 | #[derive(Serialize, Deserialize, TS)] 7 | struct Foo { 8 | a: i32, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, TS)] 12 | struct Bar { 13 | a: i32, 14 | } 15 | 16 | mod deser { 17 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 18 | 19 | use super::Foo; 20 | 21 | pub fn serialize(foo: &Foo, serializer: S) -> Result { 22 | foo.serialize(serializer) 23 | } 24 | 25 | pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { 26 | Foo::deserialize(deserializer) 27 | } 28 | } 29 | 30 | // This test should pass when serde-compat is disabled, 31 | // otherwise, it should fail to compile 32 | #[test] 33 | #[cfg(not(feature = "serde-compat"))] 34 | fn no_serde_compat() { 35 | #[derive(Serialize, Deserialize, TS)] 36 | struct Baz { 37 | #[serde(with = "deser")] 38 | a: Foo, 39 | } 40 | 41 | assert_eq!(Baz::inline(), "{ a: Foo, }") 42 | } 43 | 44 | #[test] 45 | fn serde_compat_as() { 46 | #[derive(Serialize, Deserialize, TS)] 47 | struct Baz { 48 | #[serde(with = "deser")] 49 | #[ts(as = "Bar")] 50 | a: Foo, 51 | } 52 | 53 | assert_eq!(Baz::inline(), "{ a: Bar, }") 54 | } 55 | 56 | #[test] 57 | fn serde_compat_type() { 58 | #[derive(Serialize, Deserialize, TS)] 59 | struct Baz { 60 | #[serde(with = "deser")] 61 | #[ts(type = "{ a: number }")] 62 | a: Foo, 63 | } 64 | 65 | assert_eq!(Baz::inline(), "{ a: { a: number }, }") 66 | } 67 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/simple.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::cell::RefCell; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "simple/")] 9 | struct Simple { 10 | a: i32, 11 | b: String, 12 | c: (i32, String, RefCell), 13 | d: Vec, 14 | e: Option, 15 | f: char, 16 | g: Option, 17 | } 18 | 19 | #[test] 20 | fn test_def() { 21 | assert_eq!( 22 | Simple::inline(), 23 | "{ a: number, b: string, c: [number, string, number], d: Array, e: string | null, f: string, g: string | null, }" 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/skip.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_imports)] 2 | 3 | use std::error::Error; 4 | 5 | use serde::Serialize; 6 | use ts_rs::TS; 7 | 8 | struct Unsupported; 9 | 10 | #[derive(TS)] 11 | #[ts(export, export_to = "skip/")] 12 | struct Skip { 13 | a: i32, 14 | b: i32, 15 | #[ts(skip)] 16 | c: String, 17 | #[ts(skip)] 18 | d: Box, 19 | } 20 | 21 | #[test] 22 | fn simple() { 23 | assert_eq!(Skip::inline(), "{ a: number, b: number, }"); 24 | } 25 | 26 | #[derive(TS)] 27 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 28 | #[ts(export, export_to = "skip/")] 29 | enum Externally { 30 | A( 31 | #[cfg_attr(feature = "serde-compat", serde(skip))] 32 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 33 | Unsupported, 34 | ), 35 | B( 36 | #[cfg_attr(feature = "serde-compat", serde(skip))] 37 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 38 | Unsupported, 39 | i32, 40 | ), 41 | C { 42 | #[cfg_attr(feature = "serde-compat", serde(skip))] 43 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 44 | x: Unsupported, 45 | }, 46 | D { 47 | #[cfg_attr(feature = "serde-compat", serde(skip))] 48 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 49 | x: Unsupported, 50 | y: i32, 51 | }, 52 | } 53 | 54 | #[test] 55 | fn externally_tagged() { 56 | // TODO: variant C should probably not generate `{}` 57 | assert_eq!( 58 | Externally::decl(), 59 | r#"type Externally = "A" | { "B": [number] } | { "C": { } } | { "D": { y: number, } };"# 60 | ); 61 | } 62 | 63 | #[derive(TS)] 64 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 65 | #[cfg_attr(feature = "serde-compat", serde(tag = "t"))] 66 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "t"))] 67 | #[ts(export, export_to = "skip/")] 68 | enum Internally { 69 | A( 70 | #[cfg_attr(feature = "serde-compat", serde(skip))] 71 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 72 | Unsupported, 73 | ), 74 | B { 75 | #[cfg_attr(feature = "serde-compat", serde(skip))] 76 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 77 | x: Unsupported, 78 | }, 79 | C { 80 | #[cfg_attr(feature = "serde-compat", serde(skip))] 81 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 82 | x: Unsupported, 83 | y: i32, 84 | }, 85 | } 86 | 87 | #[test] 88 | fn internally_tagged() { 89 | assert_eq!( 90 | Internally::decl(), 91 | r#"type Internally = { "t": "A" } | { "t": "B", } | { "t": "C", y: number, };"# 92 | ); 93 | } 94 | 95 | #[derive(TS)] 96 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 97 | #[cfg_attr(feature = "serde-compat", serde(tag = "t", content = "c"))] 98 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "t", content = "c"))] 99 | #[ts(export, export_to = "skip/")] 100 | enum Adjacently { 101 | A( 102 | #[cfg_attr(feature = "serde-compat", serde(skip))] 103 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 104 | Unsupported, 105 | ), 106 | B( 107 | #[cfg_attr(feature = "serde-compat", serde(skip))] 108 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 109 | Unsupported, 110 | i32, 111 | ), 112 | C { 113 | #[cfg_attr(feature = "serde-compat", serde(skip))] 114 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 115 | x: Unsupported, 116 | }, 117 | D { 118 | #[cfg_attr(feature = "serde-compat", serde(skip))] 119 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 120 | x: Unsupported, 121 | y: i32, 122 | }, 123 | } 124 | 125 | #[test] 126 | fn adjacently_tagged() { 127 | // TODO: variant C should probably not generate `{ .., "c": { } }` 128 | assert_eq!( 129 | Adjacently::decl(), 130 | r#"type Adjacently = { "t": "A" } | { "t": "B", "c": [number] } | { "t": "C", "c": { } } | { "t": "D", "c": { y: number, } };"# 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/slices.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[test] 6 | fn free() { 7 | assert_eq!(<[String]>::inline(), "Array") 8 | } 9 | 10 | #[derive(TS)] 11 | #[ts(export, export_to = "slices/")] 12 | struct Interface { 13 | #[allow(dead_code)] 14 | a: [i32], 15 | } 16 | 17 | #[test] 18 | fn interface() { 19 | assert_eq!(Interface::inline(), "{ a: Array, }") 20 | } 21 | 22 | #[derive(TS)] 23 | #[ts(export, export_to = "slices/")] 24 | struct InterfaceRef<'a> { 25 | #[allow(dead_code)] 26 | a: &'a [&'a str], 27 | } 28 | 29 | #[test] 30 | fn slice_ref() { 31 | assert_eq!(InterfaceRef::inline(), "{ a: Array, }") 32 | } 33 | 34 | #[derive(TS)] 35 | #[ts(export, export_to = "slices/")] 36 | struct Newtype(#[allow(dead_code)] [i32]); 37 | 38 | #[test] 39 | fn newtype() { 40 | assert_eq!(Newtype::inline(), "Array") 41 | } 42 | 43 | // Since slices usually need to be wrapped in a `Box` or other container, 44 | // these tests should to check for that 45 | 46 | #[test] 47 | fn boxed_free() { 48 | assert_eq!(>::inline(), "Array") 49 | } 50 | 51 | #[derive(TS)] 52 | #[ts(export, export_to = "slices/")] 53 | struct InterfaceBoxed { 54 | #[allow(dead_code)] 55 | a: Box<[i32]>, 56 | } 57 | 58 | #[test] 59 | fn boxed_interface() { 60 | assert_eq!(InterfaceBoxed::inline(), "{ a: Array, }") 61 | } 62 | 63 | #[derive(TS)] 64 | #[ts(export, export_to = "slices/")] 65 | struct NewtypeBoxed(#[allow(dead_code)] Box<[i32]>); 66 | 67 | #[test] 68 | fn boxed_newtype() { 69 | assert_eq!(NewtypeBoxed::inline(), "Array") 70 | } 71 | 72 | #[derive(TS)] 73 | #[ts(export, export_to = "slices/nested/")] 74 | struct InnerMost; 75 | 76 | #[derive(TS)] 77 | #[ts(export, export_to = "slices/nested/")] 78 | struct Nested<'a>(&'a [InnerMost]); 79 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/struct_rename.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(dead_code)] 3 | 4 | use ts_rs::TS; 5 | 6 | #[derive(TS)] 7 | #[ts(export, export_to = "struct_rename/", rename_all = "UPPERCASE")] 8 | struct RenameAllUpper { 9 | a: i32, 10 | b: i32, 11 | } 12 | 13 | #[test] 14 | fn rename_all() { 15 | assert_eq!(RenameAllUpper::inline(), "{ A: number, B: number, }"); 16 | } 17 | 18 | #[derive(TS)] 19 | #[ts(export, export_to = "struct_rename/", rename_all = "camelCase")] 20 | struct RenameAllCamel { 21 | crc32c_hash: i32, 22 | b: i32, 23 | alreadyCamelCase: i32, 24 | } 25 | 26 | #[test] 27 | fn rename_all_camel_case() { 28 | assert_eq!( 29 | RenameAllCamel::inline(), 30 | "{ crc32cHash: number, b: number, alreadyCamelCase: number, }" 31 | ); 32 | } 33 | 34 | #[derive(TS)] 35 | #[ts(export, export_to = "struct_rename/", rename_all = "PascalCase")] 36 | struct RenameAllPascal { 37 | crc32c_hash: i32, 38 | b: i32, 39 | } 40 | 41 | #[test] 42 | fn rename_all_pascal_case() { 43 | assert_eq!( 44 | RenameAllPascal::inline(), 45 | "{ Crc32cHash: number, B: number, }" 46 | ); 47 | } 48 | 49 | #[derive(TS, Default, serde::Serialize)] 50 | #[ts(export, export_to = "struct_rename/")] 51 | #[cfg_attr(feature = "serde-compat", serde(rename_all = "SCREAMING-KEBAB-CASE"))] 52 | #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "SCREAMING-KEBAB-CASE"))] 53 | struct RenameAllScreamingKebab { 54 | crc32c_hash: i32, 55 | some_field: i32, 56 | some_other_field: i32, 57 | } 58 | 59 | #[test] 60 | fn rename_all_screaming_kebab_case() { 61 | let rename_all = RenameAllScreamingKebab::default(); 62 | assert_eq!( 63 | RenameAllScreamingKebab::inline(), 64 | r#"{ "CRC32C-HASH": number, "SOME-FIELD": number, "SOME-OTHER-FIELD": number, }"# 65 | ); 66 | } 67 | 68 | #[derive(serde::Serialize, TS)] 69 | #[ts(export, export_to = "struct_rename/", rename_all = "camelCase")] 70 | struct RenameSerdeSpecialChar { 71 | #[serde(rename = "a/b")] 72 | b: i32, 73 | } 74 | 75 | #[cfg(feature = "serde-compat")] 76 | #[test] 77 | fn serde_rename_special_char() { 78 | assert_eq!(RenameSerdeSpecialChar::inline(), r#"{ "a/b": number, }"#); 79 | } 80 | 81 | // struct-level renames 82 | 83 | #[derive(TS)] 84 | #[ts(export, export_to = "struct_rename/")] 85 | #[ts(rename = "RenamedWithStrLiteral")] 86 | enum WithStrLiteral { 87 | A, 88 | B, 89 | C, 90 | } 91 | 92 | #[test] 93 | fn test_rename_with_str_literal() { 94 | assert_eq!( 95 | WithStrLiteral::decl(), 96 | r#"type RenamedWithStrLiteral = "A" | "B" | "C";"# 97 | ) 98 | } 99 | 100 | #[derive(TS)] 101 | #[ts(export, export_to = "struct_rename/")] 102 | #[ts(rename = format!("{}With{}", "Renamed", "StringExpression"))] 103 | enum WithStringExpression { 104 | A, 105 | B, 106 | C, 107 | } 108 | 109 | #[test] 110 | fn test_rename_with_string_expression() { 111 | assert_eq!( 112 | WithStringExpression::decl(), 113 | r#"type RenamedWithStringExpression = "A" | "B" | "C";"# 114 | ) 115 | } 116 | 117 | #[derive(TS)] 118 | #[ts(export, export_to = "struct_rename/")] 119 | #[ts(rename = &"RenamedWithStrExpression")] 120 | enum WithStrExpression { 121 | A, 122 | B, 123 | C, 124 | } 125 | 126 | #[test] 127 | fn test_rename_with_str_expression() { 128 | assert_eq!( 129 | WithStrExpression::decl(), 130 | r#"type RenamedWithStrExpression = "A" | "B" | "C";"# 131 | ) 132 | } 133 | 134 | #[derive(TS)] 135 | #[ts(export, export_to = "struct_rename/")] 136 | #[ts(rename = format!("i_am_inside_module_{}", module_path!().rsplit_once("::").unwrap().1))] 137 | enum RenameUsingModuleName { 138 | A, 139 | B, 140 | C, 141 | } 142 | 143 | #[test] 144 | fn test_rename_using_module_name() { 145 | assert_eq!( 146 | RenameUsingModuleName::decl(), 147 | r#"type i_am_inside_module_struct_rename = "A" | "B" | "C";"# 148 | ) 149 | } 150 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/struct_tag.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Serialize; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 9 | #[cfg_attr(feature = "serde-compat", serde(tag = "type"))] 10 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))] 11 | struct TaggedType { 12 | a: i32, 13 | b: i32, 14 | } 15 | 16 | #[derive(TS)] 17 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 18 | #[cfg_attr(feature = "serde-compat", serde(tag = "type"))] 19 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))] 20 | struct EmptyTaggedType {} 21 | 22 | #[test] 23 | fn test() { 24 | assert_eq!( 25 | TaggedType::inline(), 26 | "{ \"type\": \"TaggedType\", a: number, b: number, }" 27 | ); 28 | 29 | assert_eq!( 30 | EmptyTaggedType::inline(), 31 | r#"{ "type": "EmptyTaggedType", }"# 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/tokio.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "tokio-impl")] 2 | 3 | use tokio::sync::{Mutex, OnceCell, RwLock}; 4 | use ts_rs::TS; 5 | 6 | #[derive(TS)] 7 | #[ts(export, export_to = "tokio/")] 8 | #[ts(concrete(T = i32))] 9 | struct Tokio { 10 | mutex: Mutex, 11 | once_cell: OnceCell, 12 | rw_lock: RwLock, 13 | } 14 | 15 | #[test] 16 | fn tokio() { 17 | assert_eq!( 18 | Tokio::::decl(), 19 | "type Tokio = { mutex: number, once_cell: number, rw_lock: number, };" 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/top_level_type_as.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[cfg(feature = "serde-json-impl")] 4 | use serde_json::Value as JsonValue; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(as = "T")] 9 | pub enum UntaggedEnum { 10 | Left(T), 11 | Right(T), 12 | } 13 | 14 | #[test] 15 | pub fn top_level_type_as_enum() { 16 | assert_eq!(UntaggedEnum::::inline(), r#"string"#) 17 | } 18 | 19 | #[derive(TS)] 20 | #[ts(as = "T")] 21 | pub struct Wrapper(T); 22 | 23 | #[test] 24 | pub fn top_level_type_as_struct() { 25 | assert_eq!(Wrapper::::inline(), r#"string"#) 26 | } 27 | 28 | #[cfg(feature = "serde-json-impl")] 29 | #[derive(TS)] 30 | #[ts( 31 | export, 32 | export_to = "top_level_type_as/", 33 | as = "HashMap::" 34 | )] 35 | pub struct JsonMap(JsonValue); 36 | 37 | #[derive(TS)] 38 | #[ts(export, export_to = "top_level_type_as/")] 39 | pub struct Foo { 40 | x: i32, 41 | } 42 | 43 | #[derive(TS)] 44 | #[ts(export, export_to = "top_level_type_as/")] 45 | pub struct Bar { 46 | foo: Foo, 47 | } 48 | 49 | #[derive(TS)] 50 | #[ts( 51 | export, 52 | export_to = "top_level_type_as/", 53 | as = "HashMap::" 54 | )] 55 | pub struct Biz(String); 56 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/top_level_type_override.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | #[derive(TS)] 4 | #[ts(export, export_to = "top_level_type_override/")] 5 | #[ts(type = "string")] 6 | #[non_exhaustive] 7 | pub enum IncompleteEnum { 8 | Foo, 9 | Bar, 10 | Baz, 11 | // more 12 | } 13 | 14 | #[test] 15 | pub fn top_level_type_override_enum() { 16 | assert_eq!(IncompleteEnum::inline(), r#"string"#) 17 | } 18 | 19 | #[derive(TS)] 20 | #[ts(export, export_to = "top_level_type_override/")] 21 | #[ts(type = "string")] 22 | pub struct DataUrl { 23 | pub mime: String, 24 | pub contents: Vec, 25 | } 26 | 27 | #[test] 28 | pub fn top_level_type_override_struct() { 29 | assert_eq!(DataUrl::inline(), r#"string"#) 30 | } 31 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/tuple.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[test] 6 | fn test_tuple() { 7 | type Tuple = (String, i32, (i32, i32)); 8 | assert_eq!("[string, number, [number, number]]", Tuple::name()); 9 | } 10 | 11 | #[test] 12 | #[should_panic] 13 | fn test_decl() { 14 | type Tuple = (String, i32, (i32, i32)); 15 | let _ = Tuple::decl(); 16 | } 17 | 18 | #[test] 19 | fn test_newtype() { 20 | #[derive(TS)] 21 | struct NewType(String); 22 | 23 | assert_eq!("type NewType = string;", NewType::decl()); 24 | } 25 | 26 | #[derive(TS)] 27 | #[ts(export, export_to = "tuple/")] 28 | struct TupleNewType(String, i32, (i32, i32)); 29 | 30 | #[test] 31 | fn test_tuple_newtype() { 32 | assert_eq!( 33 | "type TupleNewType = [string, number, [number, number]];", 34 | TupleNewType::decl() 35 | ) 36 | } 37 | 38 | #[derive(TS)] 39 | #[ts(export, export_to = "tuple/")] 40 | struct Dep1; 41 | 42 | #[derive(TS)] 43 | #[ts(export, export_to = "tuple/")] 44 | struct Dep2; 45 | 46 | #[derive(TS)] 47 | #[ts(export, export_to = "tuple/")] 48 | struct Dep3; 49 | 50 | #[derive(TS)] 51 | #[ts(export, export_to = "tuple/")] 52 | struct Dep4 { 53 | a: (T, T), 54 | b: (T, T), 55 | } 56 | 57 | #[derive(TS)] 58 | #[ts(export, export_to = "tuple/")] 59 | struct TupleWithDependencies(Dep1, Dep2, Dep4); 60 | 61 | #[test] 62 | fn tuple_with_dependencies() { 63 | assert_eq!( 64 | "type TupleWithDependencies = [Dep1, Dep2, Dep4];", 65 | TupleWithDependencies::decl() 66 | ); 67 | } 68 | 69 | #[derive(TS)] 70 | #[ts(export, export_to = "tuple/")] 71 | struct StructWithTuples { 72 | a: (Dep1, Dep1), 73 | b: (Dep2, Dep2), 74 | c: (Dep4, Dep4), 75 | } 76 | 77 | #[test] 78 | fn struct_with_tuples() { 79 | assert_eq!( 80 | "type StructWithTuples = { \ 81 | a: [Dep1, Dep1], \ 82 | b: [Dep2, Dep2], \ 83 | c: [Dep4, Dep4], \ 84 | };", 85 | StructWithTuples::decl() 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/type_as.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{ 4 | cell::UnsafeCell, mem::MaybeUninit, ptr::NonNull, sync::atomic::AtomicPtr, time::Instant, 5 | }; 6 | 7 | use serde::Serialize; 8 | use ts_rs::TS; 9 | 10 | type Unsupported = UnsafeCell>>>; 11 | 12 | #[derive(TS)] 13 | #[ts(export, export_to = "type_as/")] 14 | struct ExternalTypeDef { 15 | a: i32, 16 | b: i32, 17 | c: i32, 18 | } 19 | 20 | #[derive(TS)] 21 | #[ts(export, export_to = "type_as/")] 22 | struct Override { 23 | a: i32, 24 | #[ts(as = "ExternalTypeDef")] 25 | #[ts(inline)] 26 | x: Instant, 27 | // here, 'as' just behaves like 'type' (though it adds a dependency!) 28 | #[ts(as = "ExternalTypeDef")] 29 | y: Unsupported, 30 | #[ts(as = "(i32, ExternalTypeDef, i32)")] 31 | z: Unsupported, 32 | } 33 | 34 | #[test] 35 | fn struct_properties() { 36 | assert_eq!( 37 | Override::inline(), 38 | "{ \ 39 | a: number, \ 40 | x: { a: number, b: number, c: number, }, \ 41 | y: ExternalTypeDef, \ 42 | z: [number, ExternalTypeDef, number], \ 43 | }" 44 | ); 45 | assert!(Override::dependencies() 46 | .iter() 47 | .any(|d| d.ts_name == "ExternalTypeDef")); 48 | } 49 | 50 | #[derive(TS)] 51 | #[ts(export, export_to = "type_as/")] 52 | enum OverrideEnum { 53 | A(#[ts(as = "ExternalTypeDef")] Instant), 54 | B { 55 | #[ts(as = "ExternalTypeDef")] 56 | x: Unsupported, 57 | y: i32, 58 | z: i32, 59 | }, 60 | } 61 | 62 | mod deser { 63 | use serde::{Serialize, Serializer}; 64 | 65 | use super::Instant; 66 | pub fn serialize(field: &Instant, serializer: S) -> Result { 67 | #[derive(Serialize)] 68 | struct Foo { 69 | x: i32, 70 | } 71 | Foo { x: 0 }.serialize(serializer) 72 | } 73 | } 74 | 75 | #[derive(TS)] 76 | struct OverrideVariantDef { 77 | x: i32, 78 | } 79 | 80 | #[derive(TS, Serialize)] 81 | #[ts(export, export_to = "type_as/")] 82 | enum OverrideVariant { 83 | #[ts(as = "OverrideVariantDef")] 84 | #[serde(with = "deser")] 85 | A { 86 | x: Instant, 87 | }, 88 | B { 89 | y: i32, 90 | z: i32, 91 | }, 92 | } 93 | 94 | #[test] 95 | fn enum_variants() { 96 | let a = OverrideVariant::A { x: Instant::now() }; 97 | assert_eq!(serde_json::to_string(&a).unwrap(), r#"{"A":{"x":0}}"#); 98 | assert_eq!( 99 | OverrideEnum::inline(), 100 | r#"{ "A": ExternalTypeDef } | { "B": { x: ExternalTypeDef, y: number, z: number, } }"# 101 | ); 102 | 103 | assert_eq!( 104 | OverrideVariant::inline(), 105 | r#"{ "A": OverrideVariantDef } | { "B": { y: number, z: number, } }"# 106 | ); 107 | } 108 | 109 | #[derive(TS)] 110 | #[ts(export, export_to = "type_as/")] 111 | struct Outer { 112 | #[ts(as = "Option")] 113 | #[ts(optional = nullable, inline)] 114 | x: Unsupported, 115 | #[ts(as = "Option")] 116 | #[ts(optional = nullable)] 117 | y: Unsupported, 118 | } 119 | 120 | #[test] 121 | fn complex() { 122 | let external = ExternalTypeDef::inline(); 123 | assert_eq!( 124 | Outer::inline(), 125 | format!(r#"{{ x?: {external} | null, y?: ExternalTypeDef | null, }}"#) 126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/type_override.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::time::Instant; 4 | 5 | #[cfg(feature = "serde-compat")] 6 | use serde::Serialize; 7 | use ts_rs::TS; 8 | 9 | struct Unsupported(T); 10 | struct Unsupported2; 11 | 12 | #[derive(TS)] 13 | #[ts(export, export_to = "type_override/")] 14 | struct Override { 15 | a: i32, 16 | #[ts(type = "0 | 1 | 2")] 17 | b: i32, 18 | #[ts(type = "string")] 19 | x: Instant, 20 | #[ts(type = "string")] 21 | y: Unsupported>, 22 | #[ts(type = "string | null")] 23 | z: Option, 24 | } 25 | 26 | #[test] 27 | fn simple() { 28 | assert_eq!( 29 | Override::inline(), 30 | "{ a: number, b: 0 | 1 | 2, x: string, y: string, z: string | null, }" 31 | ) 32 | } 33 | 34 | #[derive(TS)] 35 | #[ts(export, export_to = "type_override/")] 36 | struct New1(#[ts(type = "string")] Unsupported2); 37 | 38 | #[derive(TS)] 39 | #[ts(export, export_to = "type_override/")] 40 | struct New2(#[ts(type = "string | null")] Unsupported); 41 | 42 | #[test] 43 | fn newtype() { 44 | assert_eq!(New1::inline(), r#"string"#); 45 | assert_eq!(New2::inline(), r#"string | null"#); 46 | } 47 | 48 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 49 | struct S; 50 | 51 | #[derive(TS)] 52 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 53 | #[cfg_attr(feature = "serde-compat", serde(tag = "t"))] 54 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "t"))] 55 | #[ts(export, export_to = "type_override/")] 56 | enum Internal { 57 | Newtype(#[ts(type = "unknown")] S), 58 | } 59 | 60 | #[derive(TS)] 61 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 62 | #[cfg_attr(feature = "serde-compat", serde(tag = "t", content = "c"))] 63 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "t", content = "c"))] 64 | #[ts(export, export_to = "type_override/")] 65 | enum Adjacent { 66 | Newtype(#[ts(type = "unknown")] S), 67 | } 68 | 69 | #[test] 70 | fn enum_newtype_representations() { 71 | // regression test for https://github.com/Aleph-Alpha/ts-rs/issues/126 72 | assert_eq!(Internal::inline(), r#"{ "t": "Newtype" } & unknown"#); 73 | assert_eq!(Adjacent::inline(), r#"{ "t": "Newtype", "c": unknown }"#); 74 | } 75 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/union.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "union/")] 7 | enum SimpleEnum { 8 | #[ts(rename = "asdf")] 9 | A, 10 | B, 11 | C, 12 | r#D, 13 | } 14 | 15 | #[test] 16 | fn test_empty() { 17 | #[derive(TS)] 18 | enum Empty {} 19 | 20 | assert_eq!(Empty::decl(), "type Empty = never;") 21 | } 22 | 23 | #[test] 24 | fn test_simple_enum() { 25 | assert_eq!( 26 | SimpleEnum::decl(), 27 | r#"type SimpleEnum = "asdf" | "B" | "C" | "D";"# 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/union_named_serde_skip.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Deserialize; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 9 | #[cfg_attr(feature = "serde-compat", serde(untagged))] 10 | #[cfg_attr(not(feature = "serde-compat"), ts(untagged))] 11 | #[ts(export, export_to = "union_named_serde/")] 12 | enum TestUntagged { 13 | A, // serde_json -> `null` 14 | B(), // serde_json -> `[]` 15 | C { 16 | #[cfg_attr(feature = "serde-compat", serde(skip))] 17 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 18 | val: i32, 19 | }, // serde_json -> `{}` 20 | } 21 | 22 | #[derive(TS)] 23 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 24 | #[ts(export, export_to = "union_named_serde/")] 25 | enum TestExternally { 26 | A, // serde_json -> `"A"` 27 | B(), // serde_json -> `{"B":[]}` 28 | C { 29 | #[cfg_attr(feature = "serde-compat", serde(skip))] 30 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 31 | val: i32, 32 | }, // serde_json -> `{"C":{}}` 33 | } 34 | 35 | #[derive(TS)] 36 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 37 | #[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "content"))] 38 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "content"))] 39 | #[ts(export, export_to = "union_named_serde/")] 40 | enum TestAdjacently { 41 | A, // serde_json -> `{"type":"A"}` 42 | B(), // serde_json -> `{"type":"B","content":[]}` 43 | C { 44 | #[cfg_attr(feature = "serde-compat", serde(skip))] 45 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 46 | val: i32, 47 | }, // serde_json -> `{"type":"C","content":{}}` 48 | } 49 | 50 | #[derive(TS)] 51 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 52 | #[cfg_attr(feature = "serde-compat", serde(tag = "type"))] 53 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))] 54 | #[ts(export, export_to = "union_named_serde/")] 55 | enum TestInternally { 56 | A, // serde_json -> `{"type":"A"}` 57 | B, // serde_json -> `{"type":"B"}` 58 | C { 59 | #[cfg_attr(feature = "serde-compat", serde(skip))] 60 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 61 | val: i32, 62 | }, // serde_json -> `{"type":"C"}` 63 | } 64 | 65 | #[test] 66 | fn test() { 67 | assert_eq!( 68 | TestUntagged::decl(), 69 | r#"type TestUntagged = null | never[] | { };"# 70 | ); 71 | 72 | assert_eq!( 73 | TestExternally::decl(), 74 | r#"type TestExternally = "A" | { "B": never[] } | { "C": { } };"# 75 | ); 76 | 77 | assert_eq!( 78 | TestAdjacently::decl(), 79 | r#"type TestAdjacently = { "type": "A" } | { "type": "B", "content": never[] } | { "type": "C", "content": { } };"# 80 | ); 81 | 82 | assert_eq!( 83 | TestInternally::decl(), 84 | r#"type TestInternally = { "type": "A" } | { "type": "B" } | { "type": "C", };"# 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/union_rename.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ts_rs::TS; 4 | 5 | #[derive(TS)] 6 | #[ts(export, export_to = "union_rename/")] 7 | #[ts(rename_all = "lowercase", rename = "SimpleEnum")] 8 | enum RenamedEnum { 9 | #[ts(rename = "ASDF")] 10 | A, 11 | #[ts(rename = &"BB")] 12 | B, 13 | #[ts(rename = "C".repeat(2))] 14 | C, 15 | } 16 | 17 | #[test] 18 | fn test_simple_enum() { 19 | assert_eq!( 20 | RenamedEnum::decl(), 21 | r#"type SimpleEnum = "ASDF" | "BB" | "CC";"# 22 | ) 23 | } 24 | 25 | #[derive(TS)] 26 | #[ts(export, export_to = "union_rename/")] 27 | #[ts(rename = format!("{}With{}", "Renamed", "StringExpression"))] 28 | enum WithStringExpression { 29 | A, 30 | B, 31 | C, 32 | } 33 | 34 | #[test] 35 | fn test_rename_with_string_expression() { 36 | assert_eq!( 37 | WithStringExpression::decl(), 38 | r#"type RenamedWithStringExpression = "A" | "B" | "C";"# 39 | ) 40 | } 41 | 42 | #[derive(TS)] 43 | #[ts(export, export_to = "union_rename/")] 44 | #[ts(rename = &"RenamedWithStrExpression")] 45 | enum WithStrExpression { 46 | A, 47 | B, 48 | C, 49 | } 50 | 51 | #[test] 52 | fn test_rename_with_str_expression() { 53 | assert_eq!( 54 | WithStrExpression::decl(), 55 | r#"type RenamedWithStrExpression = "A" | "B" | "C";"# 56 | ) 57 | } 58 | 59 | #[derive(TS)] 60 | #[ts(export, export_to = "union_rename/")] 61 | #[ts(rename = format!("i_am_inside_module_{}", module_path!().rsplit_once("::").unwrap().1))] 62 | enum RenameUsingModuleName { 63 | A, 64 | B, 65 | C, 66 | } 67 | 68 | #[test] 69 | fn test_rename_using_module_name() { 70 | assert_eq!( 71 | RenameUsingModuleName::decl(), 72 | r#"type i_am_inside_module_union_rename = "A" | "B" | "C";"# 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/union_serde.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Deserialize; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 9 | #[cfg_attr(feature = "serde-compat", serde(tag = "kind", content = "d"))] 10 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "kind", content = "d"))] 11 | #[ts(export, export_to = "union_serde/")] 12 | enum SimpleEnum { 13 | A, 14 | B, 15 | } 16 | 17 | #[derive(TS)] 18 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 19 | #[cfg_attr(feature = "serde-compat", serde(tag = "kind", content = "data"))] 20 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "kind", content = "data"))] 21 | #[ts(export, export_to = "union_serde/")] 22 | enum ComplexEnum { 23 | A, 24 | B { foo: String, bar: f64 }, 25 | W(SimpleEnum), 26 | F { nested: SimpleEnum }, 27 | T(i32, SimpleEnum), 28 | } 29 | 30 | #[derive(TS)] 31 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 32 | #[cfg_attr(feature = "serde-compat", serde(untagged))] 33 | #[cfg_attr(not(feature = "serde-compat"), ts(untagged))] 34 | #[ts(export, export_to = "union_serde/")] 35 | enum Untagged { 36 | Foo(String), 37 | Bar(i32), 38 | None, 39 | } 40 | 41 | #[test] 42 | fn test_serde_enum() { 43 | assert_eq!( 44 | SimpleEnum::decl(), 45 | r#"type SimpleEnum = { "kind": "A" } | { "kind": "B" };"# 46 | ); 47 | assert_eq!( 48 | ComplexEnum::decl(), 49 | r#"type ComplexEnum = { "kind": "A" } | { "kind": "B", "data": { foo: string, bar: number, } } | { "kind": "W", "data": SimpleEnum } | { "kind": "F", "data": { nested: SimpleEnum, } } | { "kind": "T", "data": [number, SimpleEnum] };"# 50 | ); 51 | 52 | assert_eq!( 53 | Untagged::decl(), 54 | r#"type Untagged = string | number | null;"# 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/union_unnamed_serde_skip.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Deserialize; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 9 | #[cfg_attr(feature = "serde-compat", serde(untagged))] 10 | #[cfg_attr(not(feature = "serde-compat"), ts(untagged))] 11 | #[ts(export, export_to = "union_unnamed_serde/")] 12 | enum TestUntagged { 13 | A, // serde_json -> `null` 14 | B(), // serde_json -> `[]` 15 | C( 16 | #[cfg_attr(feature = "serde-compat", serde(skip))] 17 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 18 | i32, 19 | ), // serde_json -> `null` 20 | } 21 | 22 | #[derive(TS)] 23 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 24 | #[ts(export, export_to = "union_unnamed_serde/")] 25 | enum TestExternally { 26 | A, // serde_json -> `"A"` 27 | B(), // serde_json -> `{"B":[]}` 28 | C( 29 | #[cfg_attr(feature = "serde-compat", serde(skip))] 30 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 31 | i32, 32 | ), // serde_json -> `"C"` 33 | } 34 | 35 | #[derive(TS)] 36 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 37 | #[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "content"))] 38 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "content"))] 39 | #[ts(export, export_to = "union_unnamed_serde/")] 40 | enum TestAdjacently { 41 | A, // serde_json -> `{"type":"A"}` 42 | B(), // serde_json -> `{"type":"B","content":[]}` 43 | C( 44 | #[cfg_attr(feature = "serde-compat", serde(skip))] 45 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 46 | i32, 47 | ), // serde_json -> `{"type":"C"}` 48 | } 49 | 50 | #[derive(TS)] 51 | #[cfg_attr(feature = "serde-compat", derive(Deserialize))] 52 | #[cfg_attr(feature = "serde-compat", serde(tag = "type"))] 53 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))] 54 | #[ts(export, export_to = "union_unnamed_serde/")] 55 | enum TestInternally { 56 | A, // serde_json -> `{"type":"A"}` 57 | B, // serde_json -> `{"type":"B"}` 58 | C( 59 | #[cfg_attr(feature = "serde-compat", serde(skip))] 60 | #[cfg_attr(not(feature = "serde-compat"), ts(skip))] 61 | i32, 62 | ), // serde_json -> `{"type":"C"}` 63 | } 64 | 65 | #[test] 66 | fn test() { 67 | assert_eq!( 68 | TestUntagged::decl(), 69 | r#"type TestUntagged = null | never[] | null;"# 70 | ); 71 | 72 | assert_eq!( 73 | TestExternally::decl(), 74 | r#"type TestExternally = "A" | { "B": never[] } | "C";"# 75 | ); 76 | 77 | assert_eq!( 78 | TestAdjacently::decl(), 79 | r#"type TestAdjacently = { "type": "A" } | { "type": "B", "content": never[] } | { "type": "C" };"# 80 | ); 81 | 82 | assert_eq!( 83 | TestInternally::decl(), 84 | r#"type TestInternally = { "type": "A" } | { "type": "B" } | { "type": "C" };"# 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/union_with_data.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Serialize; 5 | use ts_rs::{Dependency, TS}; 6 | 7 | #[derive(TS)] 8 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 9 | #[ts(export, export_to = "union_with_data/")] 10 | struct Bar { 11 | field: i32, 12 | } 13 | 14 | #[derive(TS)] 15 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 16 | #[ts(export, export_to = "union_with_data/")] 17 | struct Foo { 18 | bar: Bar, 19 | } 20 | 21 | #[derive(TS)] 22 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 23 | #[ts(export, export_to = "union_with_data/")] 24 | enum SimpleEnum { 25 | A(String), 26 | B(i32), 27 | C, 28 | D(String, i32), 29 | E(Foo), 30 | F { a: i32, b: String }, 31 | } 32 | 33 | #[test] 34 | fn test_stateful_enum() { 35 | assert_eq!(Bar::decl(), r#"type Bar = { field: number, };"#); 36 | assert_eq!(Bar::dependencies(), vec![]); 37 | 38 | assert_eq!(Foo::decl(), r#"type Foo = { bar: Bar, };"#); 39 | assert_eq!( 40 | Foo::dependencies(), 41 | vec![Dependency::from_ty::().unwrap()] 42 | ); 43 | 44 | assert_eq!( 45 | SimpleEnum::decl(), 46 | r#"type SimpleEnum = { "A": string } | { "B": number } | "C" | { "D": [string, number] } | { "E": Foo } | { "F": { a: number, b: string, } };"# 47 | ); 48 | assert!(SimpleEnum::dependencies() 49 | .into_iter() 50 | .all(|dep| dep == Dependency::from_ty::().unwrap()),); 51 | } 52 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/union_with_internal_tag.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, clippy::disallowed_names)] 2 | 3 | #[cfg(feature = "serde-compat")] 4 | use serde::Serialize; 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 9 | #[cfg_attr(feature = "serde-compat", serde(tag = "type"))] 10 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))] 11 | #[ts(export, export_to = "union_with_internal_tag/")] 12 | enum EnumWithInternalTag { 13 | A { foo: String }, 14 | B { bar: i32 }, 15 | } 16 | 17 | #[derive(TS)] 18 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 19 | #[ts(export, export_to = "union_with_internal_tag/")] 20 | struct InnerA { 21 | foo: String, 22 | } 23 | 24 | #[derive(TS)] 25 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 26 | #[ts(export, export_to = "union_with_internal_tag/")] 27 | struct InnerB { 28 | bar: i32, 29 | } 30 | 31 | #[derive(TS)] 32 | #[cfg_attr(feature = "serde-compat", derive(Serialize))] 33 | #[cfg_attr(feature = "serde-compat", serde(tag = "type"))] 34 | #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))] 35 | #[ts(export, export_to = "union_with_internal_tag/")] 36 | enum EnumWithInternalTag2 { 37 | A(InnerA), 38 | B(InnerB), 39 | } 40 | 41 | #[test] 42 | fn test_enums_with_internal_tags() { 43 | assert_eq!( 44 | EnumWithInternalTag::decl(), 45 | r#"type EnumWithInternalTag = { "type": "A", foo: string, } | { "type": "B", bar: number, };"# 46 | ); 47 | 48 | assert_eq!( 49 | EnumWithInternalTag2::decl(), 50 | r#"type EnumWithInternalTag2 = { "type": "A" } & InnerA | { "type": "B" } & InnerB;"# 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/unit.rs: -------------------------------------------------------------------------------- 1 | use ts_rs::TS; 2 | 3 | // serde_json serializes this to `null`, so it's TS type is `null` as well. 4 | #[derive(TS)] 5 | #[ts(export, export_to = "unit/")] 6 | struct Unit; 7 | 8 | // serde_json serializes this to `{}`. 9 | // The TS type best describing an empty object is `Record`. 10 | #[derive(TS)] 11 | #[ts(export, export_to = "unit/")] 12 | struct Unit2 {} 13 | 14 | // serde_json serializes this to `[]`. 15 | // The TS type best describing an empty array is `never[]`. 16 | #[derive(TS)] 17 | #[ts(export, export_to = "unit/")] 18 | struct Unit3(); 19 | 20 | // serde_json serializes this to `null`, so it's TS type is `null` as well. 21 | #[derive(TS)] 22 | #[ts(export, export_to = "unit/")] 23 | struct Unit4(()); 24 | 25 | #[test] 26 | fn test() { 27 | assert_eq!("type Unit = null;", Unit::decl()); 28 | assert_eq!("type Unit2 = Record;", Unit2::decl()); 29 | assert_eq!("type Unit3 = never[];", Unit3::decl()); 30 | assert_eq!("type Unit4 = null;", Unit4::decl()); 31 | } 32 | -------------------------------------------------------------------------------- /ts-rs/tests/integration/unsized.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{borrow::Cow, rc::Rc, sync::Arc}; 4 | 5 | use ts_rs::TS; 6 | 7 | #[derive(TS)] 8 | #[ts(export, export_to = "unsized/")] 9 | struct S<'a> { 10 | b: Box, 11 | c: Cow<'a, str>, 12 | r: Rc, 13 | a: Arc, 14 | } 15 | 16 | #[test] 17 | fn contains_str() { 18 | assert_eq!( 19 | S::decl(), 20 | "type S = { b: string, c: string, r: string, a: string, };" 21 | ) 22 | } 23 | --------------------------------------------------------------------------------