├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── admin └── project │ ├── README.md │ ├── completed │ ├── add_typescript_validation_generation.md │ ├── basic_requirements.md │ ├── move_ts_str_macro_to_separate_crate.md │ ├── mutli_emitter_destinations.md │ ├── polish_publication.md │ ├── proper_rename.md │ ├── serde_attributes.md │ ├── serde_enum_tuples.md │ ├── simple_enums.md │ ├── support_testing.md │ └── support_type_generator_state.md │ ├── in_progress │ ├── support_transparent_types.md │ └── support_tuple_structs.md │ └── planning │ ├── end_to_end_testing.md │ ├── remove_tag_requirement.md │ ├── support_aliases.md │ ├── support_arbitrary_attributes.md │ ├── support_custom_derives.md │ └── tuple_structs.md ├── publish.sh ├── ts_quote ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── ts_quote_macros ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── parsing.rs │ ├── ts_quote.rs │ └── ts_string.rs ├── type_reflect ├── Cargo.toml ├── README.md ├── examples │ └── declare_and_export │ │ ├── declare_and_export.rs │ │ └── output │ │ └── zod.ts ├── src │ ├── alias_type.rs │ ├── enum_type.rs │ ├── lib.rs │ ├── rust.rs │ ├── struct_type.rs │ ├── ts_format.rs │ ├── ts_validation │ │ ├── enum_type │ │ │ ├── case_type.rs │ │ │ ├── complex.rs │ │ │ ├── mod.rs │ │ │ └── untagged │ │ │ │ ├── case_type.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── union_case.rs │ │ │ │ └── unit_case.rs │ │ ├── mod.rs │ │ ├── struct_type.rs │ │ └── validation │ │ │ ├── array.rs │ │ │ ├── map.rs │ │ │ ├── mod.rs │ │ │ ├── primitive.rs │ │ │ ├── tuple.rs │ │ │ └── type_validation.rs │ ├── type_script │ │ ├── alias_type.rs │ │ ├── enum_type.rs │ │ ├── mod.rs │ │ ├── struct_type.rs │ │ ├── type_fields.rs │ │ └── untagged_enum_type.rs │ └── zod │ │ ├── alias_type.rs │ │ ├── enum_type.rs │ │ ├── mod.rs │ │ └── struct_type.rs └── tests │ ├── .gitignore │ ├── common.rs │ ├── output │ ├── jest.config.js │ ├── package.json │ ├── tsconfig.json │ └── yarn.lock │ ├── test_adt.rs │ ├── test_array.rs │ ├── test_boxed.rs │ ├── test_case_inflection.rs │ ├── test_map.rs │ ├── test_nested.rs │ ├── test_optional.rs │ ├── test_simple_enum.rs │ ├── test_simple_struct.rs │ ├── test_struct_types.rs │ ├── test_ts_quote.rs │ ├── test_ts_string.rs │ └── test_untagged_enum.rs ├── type_reflect_core ├── Cargo.toml ├── README.md └── src │ ├── inflection.rs │ ├── lib.rs │ └── type_description.rs └── type_reflect_macros ├── Cargo.toml ├── README.md └── src ├── attribute_utils.rs ├── export_types_impl ├── destination.rs └── mod.rs ├── lib.rs ├── type_def ├── enum_def.rs ├── mod.rs ├── struct_def.rs ├── syn_type_utils.rs ├── type_alias_def.rs └── type_utils.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | node_modules/ 4 | type_reflect/example_output 5 | type_reflect/examples/*/output/* 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "ts_quote_macros", 4 | "ts_quote", 5 | "type_reflect_macros", 6 | "type_reflect_core", 7 | "type_reflect", 8 | ] 9 | 10 | resolver = "2" 11 | 12 | [patch.crates-io] 13 | ts_quote_macros = { path = "ts_quote_macros" } 14 | ts_quote = { path = "ts_quote" } 15 | type_reflect_core = { path = "type_reflect_core" } 16 | type_reflect_macros = { path = "type_reflect_macros" } 17 | type_reflect = { path = "type_reflect" } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Type Reflect Monorepo 2 | 3 | This is the monorepo for `type_reflect` and `ts_quote`. It contains two main projects: 4 | 5 | - [`type_reflect`](#1--type-reflect): Runtime reflection for Rust types, to facilitate easy bridging between Rust types and other languages 6 | - [`ts_quote`](#2-%EF%B8%8F-ts-quote): Utilities for generating TypeScript code from Rust 7 | 8 | ## 1. 🪩 Type Reflect 9 | 10 | `type_reflect` provides procedural macros to allow for runtime reflection on Rust types. It's main goal is to facilitate sharing serializable types between languages, for example in the use-case of sharing types between a Rust webservice, consumed by a TypeScript client. 11 | 12 | It provides some utilities out of the box for exporting Rust types to TypeScript, both as raw TS types and Zod schemas, and is designed to be extensible, so the user can implement custom type exporters to meet thier own specific use-case. 13 | 14 |
📝 Example usage 15 | 16 | Give types runtime reflection using the `Reflect` derive macro: 17 | 18 | ```rust 19 | #[derive(Reflect)] 20 | struct Message { 21 | index: u32, 22 | text: Option, 23 | } 24 | ``` 25 | 26 | Export types using the `export_types!` macro: 27 | 28 | ```rust 29 | export_types!( 30 | types: [ 31 | Message 32 | ] 33 | exports: [ 34 | Zod("/path/to/zod_export.ts"), 35 | TypeScript("/path/to/ts_export.ts", tab_width: 2), 36 | ] 37 | ) 38 | ``` 39 | 40 | Invoking this macro will generate the following `ts_export.ts` file: 41 | 42 | ```ts 43 | export type Message = { 44 | index: number; 45 | text?: string; 46 | }; 47 | ``` 48 | 49 | and the following `zod_export.ts`: 50 | 51 | ```ts 52 | import { z } from 'zod'; 53 | 54 | export const MessageSchema = z.object({ 55 | index: z.number(), 56 | text: z.string().optional(), 57 | }); 58 | 59 | export type Message = z.infer; 60 | ``` 61 | 62 | *For more examples check the [type_reflect crate README](type_reflect)* 63 | 64 |
65 | 66 | ### 📦 Type Reflect Crates: 67 | 68 | | Crate | Description | Links | 69 | |----------|-------------|----------| 70 | | → `type_reflect` ← | The main `type_reflect` crate for public consumption. | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](type_reflect) [![Crates.io](https://img.shields.io/crates/v/type_reflect.svg)](https://crates.io/crates/type_reflect) [![Documentation](https://docs.rs/type_reflect/badge.svg)](https://docs.rs/type_reflect) | 71 | | `type_reflect_macros` | Procedural macro implementations for `type_reflect`. This crate is for internal use, and the macros are re-exported by the `type_reflect` crate. | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](type_reflect_macros) [![Crates.io](https://img.shields.io/crates/v/type_reflect_macros.svg)](https://crates.io/crates/type_reflect_macros) [![Documentation](https://docs.rs/type_reflect_macros/badge.svg)](https://docs.rs/type_reflect_macros) | 72 | | `type_reflect_core` | A crate for shared components used by both `type_reflect` and `type_reflect_macros`. | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](type_reflect_core) [![Crates.io](https://img.shields.io/crates/v/type_reflect_core.svg)](https://crates.io/crates/type_reflect_core) [![Documentation](https://docs.rs/type_reflect_core/badge.svg)](https://docs.rs/type_reflect_core) | 73 | 74 | ## 2. 🖊️ TS Quote 75 | 76 | `ts_quote` provides procedural macros and utilities for generating TypeScript code in Rust. 77 | 78 | Usage is similar to the popular [`quote` crate](https://crates.io/crates/quote) for Rust code generation. 79 | 80 |
📝 Example usage 81 | 82 | Create a TypeScript string using the `ts_string!` macro: 83 | 84 | ```rust 85 | let ts: String = ts_string!{ const foo: number = 1; }; 86 | ``` 87 | 88 | Embed Rust runtime values in the output by prefixing with `#`: 89 | 90 | ```rust 91 | let var_name = "foo"; 92 | let value = 1; 93 | 94 | let ts: String = ts_string!{ const #var_name: number = #value; }; 95 | // the value of ts is "const foo: number = 1;" 96 | ``` 97 | 98 | Output pretty-printed TypeScript: 99 | 100 | ```rust 101 | let ts_func: TS = ts_quote! { 102 | const add = (x: number, y: number) => { 103 | return x + y; 104 | }; 105 | }?; 106 | 107 | let pretty: String = ts_func.formatted(None)?; 108 | ``` 109 | 110 | *For more examples check the [ts_quote crate README](ts_quote)* 111 | 112 |
113 | 114 | ### 📦 TS Quote Crates: 115 | 116 | | Crate | Description | Links | 117 | |----------|-------------|----------| 118 | | → `ts_quote` ← | The main `ts_quote` crate for public consumption. | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](ts_quote) [![Crates.io](https://img.shields.io/crates/v/ts_quote.svg)](https://crates.io/crates/ts_quote) [![Documentation](https://docs.rs/ts_quote/badge.svg)](https://docs.rs/ts_quote) | 119 | | `ts_quote_macros` | Procedural macro implementations for `ts_quote`. This crate is for internal use, and the macros are re-exported by the `ts_quote` crate. | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](ts_quote_macros) [![Crates.io](https://img.shields.io/crates/v/ts_quote.svg)](https://crates.io/crates/ts_quote_macros) [![Documentation](https://docs.rs/ts_quote/badge.svg)](https://docs.rs/ts_quote_macros) | 120 | -------------------------------------------------------------------------------- /admin/project/README.md: -------------------------------------------------------------------------------- 1 | This directory is used to manage the workflow for this project. It can be thought of as a lightweight Kanban system. 2 | 3 | Files in "completed" are done. 4 | 5 | Files in "in_progress" are being worked on. 6 | 7 | Files in "planning" are being refined for work. 8 | -------------------------------------------------------------------------------- /admin/project/completed/add_typescript_validation_generation.md: -------------------------------------------------------------------------------- 1 | # Typescript Validation & Parsing Generation 2 | 3 | I've tried integrating with [`typia`](https://typia.io/docs/) for a smoother experience with runtime type validation, but I am running into the blocker that the code transformation required by Typia does not work well with the compilation process implemented by Vite. 4 | 5 | Therefore, since I am implementing code generation anyway, I can create my own ahead-of-time code generation for the types I generate. 6 | 7 | ## Design 8 | 9 | For each type I generate, I want to generate two functions: 10 | 1. A parsing implementation 11 | 2. A validation implementation 12 | 13 | So for example if I have this type: 14 | 15 | ```rs 16 | struct Foo { 17 | x: f32, 18 | name: String, 19 | bar: Bar, 20 | } 21 | ``` 22 | 23 | This should generate the following functions: 24 | 25 | ```ts 26 | namespace Foo { 27 | export function validate(input: any): ValidationResult { 28 | if(!input.x) { 29 | return {ok: false, error: "Error vaildaing Foo: expected member x does not exist"}; 30 | } else { 31 | let res = validateNumber(input.x); 32 | if(!res.ok) { 33 | return {ok: false, error: `Error vaildaing Foo: ${res.error}` 34 | } 35 | } 36 | 37 | if(!input.name) { 38 | return {ok: false, error: "Error vaildaing Foo: expected member name does not exist"}; 39 | } else { 40 | let res = validateString(input.name); 41 | if(!res.ok) { 42 | return {ok: false, error: `Error vaildaing Foo: ${res.error}` 43 | } 44 | } 45 | 46 | if(!input.bar) { 47 | return {ok: false, error: "Error vaildaing Foo: expected member bar does not exist"}; 48 | } else { 49 | let res = Bar.validate(input.bar); 50 | if(!res.ok) { 51 | return {ok: false, error: `Error vaildaing Foo: ${res.error}`} 52 | } 53 | } 54 | return {ok: true}; 55 | } 56 | 57 | export function parse(input: string): ParseResult { 58 | const data = JSON.parse(input); 59 | let val = Foo.validate(data); 60 | if(!val.ok) { 61 | return val; 62 | } 63 | return { 64 | ok: true, 65 | value: data as Foo, 66 | } 67 | } 68 | } 69 | 70 | ``` 71 | 72 | ### Array Validation 73 | 74 | For an array, we need to validate that every member of the array conforms to the desired type 75 | 76 | ```ts 77 | 78 | type ArrType = { 79 | records: Array 80 | } 81 | 82 | namespace ArrType { 83 | function validate(input: any) -> Result { 84 | if(!input.records) { 85 | return {ok: false, error: `Error vaildaing ArrType: expected ArrType.records to be defined`} 86 | } else { 87 | if(!Array.isArray(input.records)) { 88 | return {ok: false, error: `Error vaildaing ArrType: expected ArrType.records to be an Array`} 89 | } 90 | for (let value in input.records) { 91 | let res = Foo.validate(input.bar); 92 | if(!res.ok) { 93 | return {ok: false, error: `Error vaildaing ArrType: ${res.error}`} 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | ``` 101 | 102 | ### Redesign 103 | 104 | After giving this some thought, I think it's better that the validator should throw errors rather than using monads. 105 | 106 | This is more idiomatic Typescript, and may even be more performant. 107 | 108 | So each type generated should generate: 109 | 110 | ```ts 111 | namespace MyType { 112 | // A validator which throws 113 | export function tryValidate(): MyType { ... } 114 | 115 | // A parser which throws 116 | export function tryParse(input: string): MyType { ... } 117 | 118 | // A validator which returns a result 119 | export function validate(): Result { ... } 120 | 121 | // A parser which returns a result 122 | export function parse(input: string): Result { ... } 123 | } 124 | ``` 125 | 126 | ### Error Types 127 | 128 | For the thrown errors, we have to cover the follwing: 129 | 130 | 1. Missing members: 131 | - "Error validating MyType.member: expected [string] found [undefined]" 132 | 133 | 2. Type mismatch: 134 | - "Error validating MyType.member: expected [string] found [number] 135 | 136 | 137 | ### Array of Types 138 | 139 | Each validator should also have the option to parse or validate an array of that type, for convenience 140 | 141 | ## TODO: 142 | 143 | - [x] Implement generation for struct types 144 | - [x] Named type keys 145 | - [x] string keys 146 | - [x] number keys 147 | - [x] bool keys 148 | - [x] option keys 149 | - [x] Array keys 150 | - [x] Map keys 151 | - [x] Implement generation for enum types 152 | - [x] Simple enums 153 | - [x] Enum variants 154 | - [x] Enum union type 155 | 156 | - [x] Add tests 157 | - [x] Simple value 158 | - [x] validation 159 | - [x] parsing 160 | - [x] Optional member 161 | - [x] Nested types 162 | - [x] Arrays 163 | - [x] Type with array 164 | - [x] Array of types 165 | - [x] Map member 166 | -------------------------------------------------------------------------------- /admin/project/completed/basic_requirements.md: -------------------------------------------------------------------------------- 1 | ToDo: 2 | 3 | - [x] Basic end-to-end 4 | - [x] Parsing types implemented 5 | - [x] Export function working 6 | - [x] Support for complex types in TS/zod export 7 | - [x] Option 8 | - [x] Array 9 | - [x] Map 10 | - [x] Support for Rust export 11 | - [x] Support for Enums 12 | - [x] Coordinate with Serde attributes 13 | - [x] Throw a validation error if the incorrect attributes are applied 14 | -------------------------------------------------------------------------------- /admin/project/completed/move_ts_str_macro_to_separate_crate.md: -------------------------------------------------------------------------------- 1 | # Move ts_str macro to it's own crate 2 | 3 | Currently I have ts_str in a general macros crate for supporting type_reflect. It makes more sense for it to be in it's own crate. 4 | 5 | ## TODO: 6 | 7 | - [x] Move `ts_str!` to the new crate 8 | - [x] Rename `ts_str!` to `ts_string!` and test 9 | - [x] Implement `ts_quote!` 10 | - [x] Implement TS type 11 | - [x] Implement `ts_quote` macro 12 | - [x] Document 13 | -------------------------------------------------------------------------------- /admin/project/completed/mutli_emitter_destinations.md: -------------------------------------------------------------------------------- 1 | # Mult-emitter destinations 2 | 3 | It might be the case that I want to use multiple emitters for the same output file. 4 | 5 | For instance, this can be helpful for generating TS validation: 6 | - First emit the Typescript types, using the TS emitter 7 | - Then emit the validaton functions, using the TSValidation emitter 8 | 9 | ## Design 10 | 11 | I can support this in the `export_types` macro, with the following pattern: 12 | 13 | ``` 14 | export_types! { 15 | types: [ Foo ], 16 | destinations: [ 17 | ( 18 | "path/1.ts", 19 | "path/2.ts", 20 | prefix: "", 21 | emitters: [ 22 | TypeScript(), 23 | TSValidation(), 24 | TSFormat(tab_size: 2) 25 | ] 26 | ) 27 | ] 28 | } 29 | ``` 30 | 31 | I.e. we introduce an "un-named" variant to the destination type, and allow multiple emitters to be passed. 32 | 33 | In this case, the code generated should be: 34 | 35 | ``` 36 | 37 | let mut file = 38 | 39 | 40 | let mut emitter = TypeScript { 41 | ..Default::default() 42 | }; 43 | let mut file = emitter 44 | .init_destination_file( 45 | "/export/dir/1", 46 | "", 47 | )?; 48 | file.write_all(emitter.emit::().as_bytes())?; 49 | emitter.finalize("/export/dir/1")?; 50 | 51 | let mut emitter = TSFormat { 52 | tab_size: 2, 53 | ..Default::default() 54 | }; 55 | file.write_all(emitter.emit::().as_bytes())?; 56 | emitter.finalize("/export/dir/1")?; 57 | let mut emitter = TSValidation { 58 | ..Default::default() 59 | }; 60 | file.write_all(emitter.emit::().as_bytes())?; 61 | emitter.finalize("/export/dir/1")?; 62 | ``` 63 | 64 | 65 | ## TODO: 66 | - [x] Move file and prefix creation out of emitter 67 | - [x] Define Un-named emitter variant 68 | - [x] Define parsing behavior 69 | - [x] Define code generation 70 | - [x] Add documentation 71 | -------------------------------------------------------------------------------- /admin/project/completed/polish_publication.md: -------------------------------------------------------------------------------- 1 | # Polish publication to Crates.io 2 | 3 | ## TODO: 4 | 5 | - [x] Publish crates 6 | - [x] Publish docs 7 | - [x] Add links to doc pages to crates 8 | - [x] Add links to github to crates 9 | - [x] Add links to crates.io & documentation on README's 10 | -------------------------------------------------------------------------------- /admin/project/completed/proper_rename.md: -------------------------------------------------------------------------------- 1 | # Proper Rename 2 | 3 | Bug: 4 | 5 | Currently type_reflect does not support the attribute #[serde(rename_all=...)] correctly. 6 | -------------------------------------------------------------------------------- /admin/project/completed/serde_attributes.md: -------------------------------------------------------------------------------- 1 | # Serde Attributes 2 | 3 | *As a developer, I want type_reflect to automatically observe the serde attributes when emitting my types, so that my typescript types can be created from the JSON produced by serializing my Rust types thorugh Serde.* 4 | 5 | # Acceptance Criteria 6 | 7 | - [x] For enum types, the serde "tag" attribute should be used as the case identifier 8 | 9 | I.e. if I have an enum decalred like so: 10 | 11 | ``` 12 | #[derive(Reflect, Serialize)] 13 | #[serde(tag="my_tag")] 14 | enum SerdeTagExample { 15 | Foo { ... } 16 | } 17 | ``` 18 | 19 | Then Foo should generate the following zod output: 20 | 21 | ``` 22 | export const SerdeTagExampleCaseFooSchema = z.object({ 23 | my_tag: SerdeTagExampleCase.Foo, 24 | ... 25 | }); 26 | export type SerdeTagExampleCaseFoo = z.infer 27 | ``` 28 | 29 | 30 | - [x] For enum types, the serde "content" attribute should be used to as the key for nested data 31 | 32 | So for instance, if we have this enum: 33 | 34 | ``` 35 | #[derive(Reflect, Serialize)] 36 | #[serde(tag="my_tag", content="data")] 37 | enum SerdeContentExample { 38 | Foo(i32) 39 | } 40 | ``` 41 | 42 | 43 | Then Foo should generate the following zod output: 44 | 45 | 46 | ``` 47 | export const SerdeContentExampleCaseFooSchema = z.object({ 48 | my_tag: SerdeContentExampleCase.Foo, 49 | content: z.number() 50 | }); 51 | export type SerdeContentExampleCaseFoo = z.infer 52 | ``` 53 | 54 | 55 | - [x] If the "tag" or "content" attribute is missing, an error should be thrown 56 | - [x] The Serde: "rename_all" attribute should be supported 57 | -------------------------------------------------------------------------------- /admin/project/completed/serde_enum_tuples.md: -------------------------------------------------------------------------------- 1 | # Serde Enum Tuples 2 | 3 | *As a developer, I want my exported tuples to match those serialized by Serde, so my typescript types are fully interoperable with Rust* 4 | 5 | ## Acceptance Criteria 6 | 7 | - [x] Tuple-type enums' associated data should generate ts as a value when there is only one item in the tuple 8 | - [x] Tuple-type enums' associated data should generate ts as a tuple when there is more than one item in the tuple 9 | -------------------------------------------------------------------------------- /admin/project/completed/simple_enums.md: -------------------------------------------------------------------------------- 1 | # Simple Enums 2 | 3 | *As a developer, I want simple enums, those without ADT's or "C like" enums, to conform to the convention used by Serde, so that I can easily share these enums between Rust and Typescript* 4 | 5 | ## Acceptance Criteria 6 | 7 | - [x] If zero enum cases contain associated data, the enum should be exported as a simple TypeScript enum 8 | 9 | So for example, if I have this enum: 10 | 11 | ``` 12 | enum SimpleEnumsExample { 13 | Foo 14 | } 15 | ``` 16 | 17 | This should be exported to ts like so: 18 | 19 | ``` 20 | export enum SimpleEnumsExample { 21 | Foo = "Foo" 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /admin/project/completed/support_testing.md: -------------------------------------------------------------------------------- 1 | # Support Testing 2 | 3 | In order to develop successfully, I have to be able to test the generated types 4 | 5 | ## TODO: 6 | 7 | - [x] Setup test project 8 | - [x] Util for generating and running test files 9 | - [x] Add postfix to export_types macro 10 | -------------------------------------------------------------------------------- /admin/project/completed/support_type_generator_state.md: -------------------------------------------------------------------------------- 1 | # Type Emitter State 2 | 3 | It would be nice to support state in type emitters, to allow for more flexibility in terms of how clients are able to generate types. 4 | 5 | ## Design 6 | 7 | Currently, `export_types!`: 8 | 9 | ```rust 10 | export_types!( 11 | types: [ 12 | ServerEvent, 13 | ClientEvent, 14 | IdentifierIDRecord, 15 | ], 16 | destinations: [ 17 | TypeScript( 18 | "./inspector_client/src/external_types/lsp_broker.ts", 19 | prefix: "export type IdentifierID = number;" 20 | ), 21 | ] 22 | ) 23 | } 24 | ``` 25 | 26 | unwraps to something like this: 27 | 28 | ```rust 29 | let mut file = TypeScript::init_destination_file( 30 | "./inspector_client/src/external_types/lsp_broker.ts", 31 | "export type IdentifierID = number;", 32 | )?; 33 | file.write_all(TypeScript::emit::().as_bytes())?; 34 | file.write_all(TypeScript::emit::().as_bytes())?; 35 | file.write_all(TypeScript::emit::().as_bytes())?; 36 | TypeScript::finalize("./inspector_client/src/external_types/lsp_broker.ts")?; 37 | ``` 38 | 39 | So in other words, we have only associated functions to handle the type emission process. 40 | 41 | It would be more powerful to pass an object to the export function, so that emitter could retain its own stat: 42 | 43 | ```rust 44 | let mut file = TypeScript::init_destination_file( 45 | "./inspector_client/src/external_types/lsp_broker.ts", 46 | "export type IdentifierID = number;", 47 | )?; 48 | 49 | let mut emitter = TypeScript {..Default::default()}; 50 | 51 | emitter::emit(file)?; 52 | emitter::emit(file)?; 53 | emitter::emit(file)?; 54 | emitter::finalize("./inspector_client/src/external_types/lsp_broker.ts")?; 55 | ``` 56 | 57 | Here we can also support the addition of forwarding of parameters to the emitter. 58 | 59 | So for instance, we could have a destination which looks like this: 60 | 61 | ```rust 62 | TypeScript( 63 | "./inspector_client/src/external_types/lsp_broker.ts", 64 | prefix: "export type IdentifierID = number;" 65 | indent_size: 2, 66 | ), 67 | ``` 68 | 69 | Generate this: 70 | 71 | ```rust 72 | let mut emitter = TypeScript { 73 | indent_size: 2, 74 | ..Default::default() 75 | }; 76 | ``` 77 | 78 | # TODO: 79 | 80 | - [x] Make emitters stateful 81 | - [x] Forward named arguments to emitters 82 | -------------------------------------------------------------------------------- /admin/project/in_progress/support_transparent_types.md: -------------------------------------------------------------------------------- 1 | # Support Transparent Types 2 | 3 | Certain types are treated as transparent when serializing and deserialzing a type using Serde. 4 | 5 | So for instance, if we have a type like so: 6 | 7 | ``` 8 | struct Foo { 9 | val: Box 10 | } 11 | 12 | let foo = Foo { Box::new(true) } 13 | ``` 14 | 15 | This will be serialized like so: 16 | 17 | ``` 18 | { 19 | "val" : true 20 | } 21 | ``` 22 | 23 | We can add support for the following transparent types: 24 | 25 | - Box 26 | - Rc 27 | - Arc 28 | - Mutex 29 | - RwLock 30 | 31 | 32 | Todo: 33 | 34 | - [ ] Add type representation for transparent types 35 | - [ ] Parse transparent types from proc macro input 36 | - [ ] Handle transparent types in the generators 37 | - [ ] typescript 38 | - [ ] ts_validation 39 | 40 | - For now we omit zod, because it's a bit harder to implement consistently 41 | -------------------------------------------------------------------------------- /admin/project/in_progress/support_tuple_structs.md: -------------------------------------------------------------------------------- 1 | # Suppoort Tuple Structs 2 | 3 | I'm running into a limitaiton with the current implementation, that Reflect doesn't support tuple-type structs. 4 | 5 | I.e. I can support structs like this: 6 | 7 | ``` 8 | struct Foo { 9 | x: u32 10 | } 11 | ``` 12 | 13 | but not like this: 14 | 15 | ``` 16 | struct Foo(u32); 17 | ``` 18 | 19 | ## Design 20 | 21 | I already support tuple type enum variants, so it should not be a huge leap to support tuple type structs 22 | 23 | How can we adapt the design? 24 | 25 | Currently we support high-level traits for Rust types: 26 | 27 | ``` 28 | trait StructType 29 | trait EnumType 30 | ``` 31 | 32 | for Structs, currently we assume that we have a set of named members. 33 | 34 | We need to expand this definition to suppprt anonymous structs, and also unit structs. 35 | 36 | So for instance, we could differentiate structs by having a different trait for each struct type: 37 | 38 | ``` 39 | trait StructTypeNamed 40 | trait StructTypeTuple 41 | trait StructTypeUnit 42 | ``` 43 | 44 | Or we could differentiate the types within the member type returned by the struct: 45 | 46 | ``` 47 | trait StructType { 48 | fn members() -> StructMembersNamed | StructMembersTuple | StructMembersUnit 49 | } 50 | ``` 51 | 52 | I think the right approach here is to re-use the `EnumCaseType` here to represent all cases of type fields. 53 | 54 | We can rename this to `TypeFieldsDefinition` 55 | 56 | ## TODO: 57 | 58 | - [x] Rename `EnumCaseType` to `TypeFieldsDefinition` 59 | - [x] Rename `TypeFieldsDefinition::Simple` to `TypeFieldsDefinition::Unit` 60 | - [x] Rename `TypeFieldsDefinition::Struct` to `TypeFieldsDefinition::Named` 61 | - [x] Rename `StructMember` to `NamedField` 62 | - [x] Rename `StructType.members` to `StructType.fields` 63 | - [x] Modify `StructType.fields` to return `TypeFieldsDefinition` 64 | - [x] Modify trait definition 65 | - [x] Modify proc macro 66 | - [x] Modify generator implementations 67 | - [ ] Implement test for tuple structs 68 | -------------------------------------------------------------------------------- /admin/project/planning/end_to_end_testing.md: -------------------------------------------------------------------------------- 1 | # End-to-end Testing 2 | 3 | It would be nice to support some automated tests for some use-cases. 4 | 5 | For example: 6 | - Generate an interface 7 | - Serialize an instance from Rust 8 | - Check with Zod 9 | - Serialize an instance from TS 10 | - Check with Rust 11 | -------------------------------------------------------------------------------- /admin/project/planning/remove_tag_requirement.md: -------------------------------------------------------------------------------- 1 | # Remove Tag Requirement 2 | 3 | Currently enums require a serde `tag` attribute when there is associated data. 4 | 5 | This requirement should be relaxed: 6 | - support the untagged union typescript case 7 | - the untagged serde representation is the default enum representation for Swift 8 | 9 | # Design: 10 | -------------------------------------------------------------------------------- /admin/project/planning/support_aliases.md: -------------------------------------------------------------------------------- 1 | # Type Alias Support 2 | 3 | *As a developer, I want to be able to delcare a type alias in Rust and use it in TS* 4 | 5 | ## Design 6 | 7 | A type alias in Rust: 8 | 9 | type Foo = String; 10 | 11 | Should output like so: 12 | 13 | export const FooScema = z.string(); 14 | export type Foo = z.infer 15 | 16 | ## Challenge 17 | 18 | In order to support this, I need to be able to run Reflect outside the context of a derive macro. Derive macros apparently don't work on type declarations in Rust. 19 | -------------------------------------------------------------------------------- /admin/project/planning/support_arbitrary_attributes.md: -------------------------------------------------------------------------------- 1 | # Arbitrary Attributes 2 | 3 | One of the feature requests which has come up for type-reflect is to support validators. 4 | 5 | I don't want to choose a "blessed" validator, but it would be nice to support arbitrary attributes, so users could add validators on their own. 6 | -------------------------------------------------------------------------------- /admin/project/planning/support_custom_derives.md: -------------------------------------------------------------------------------- 1 | # Custom Derives 2 | 3 | *As a developer, I might want my exported types to contain custom derive macros in Rust* 4 | 5 | ## Design 6 | 7 | I would like to be able to define a rust output with custom derives like so: 8 | 9 | ``` 10 | 11 | exort_types! { 12 | ... 13 | destinations: [ 14 | Rust( 15 | derrives: [ 16 | MyCustomMacro 17 | ], 18 | ... 19 | ) 20 | ] 21 | 22 | } 23 | 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /admin/project/planning/tuple_structs.md: -------------------------------------------------------------------------------- 1 | # Tuple Structs 2 | 3 | *As a developer, I want to be able to bridge tuple type structs between Rust and TS* 4 | 5 | It is currently no possible to bridge a struct like this: 6 | 7 | ``` 8 | struct MyStruct(i32, f32, String) 9 | ``` 10 | 11 | And it should be 12 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | (cd ts_quote_macros && cargo publish) 2 | (cd ts_quote && cargo publish) 3 | (cd type_reflect_core && cargo publish) 4 | (cd type_reflect_macros && cargo publish) 5 | (cd type_reflect && cargo publish) 6 | -------------------------------------------------------------------------------- /ts_quote/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ts_quote" 3 | version = "0.4.0" 4 | authors = ["Spencer Kohan "] 5 | edition = "2021" 6 | description = "Procedural macros for quasi-quoting TypeScript from Rust" 7 | license = "Apache-2.0" 8 | 9 | repository = "https://github.com/spencerkohan/type_reflect/tree/main/ts_quote" 10 | documentation = "https://docs.rs/ts_quote" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | ts_quote_macros = "0.4.0" 16 | deno_ast = "0.31.6" 17 | dprint-plugin-typescript = "0.88.3" 18 | anyhow = "1.0.75" 19 | -------------------------------------------------------------------------------- /ts_quote/README.md: -------------------------------------------------------------------------------- 1 | # TS Quote 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/ts_quote.svg)](https://crates.io/crates/ts_quote) 4 | [![Documentation](https://docs.rs/ts_quote/badge.svg)](https://docs.rs/ts_quote) 5 | 6 | *This crate is part of a larger workspace, see the [monorepo README](https://github.com/spencerkohan/type_reflect) for more details* 7 | 8 | This crate provides a few quasi-quote macros for generating TypeScript from inside Rust. 9 | 10 | It is built upon the [`Deno`](https://deno.com) project, and is interoperable with Deno's TypeScript representation. 11 | 12 | The interface is heavily inspired by the popular [`quote` crate](https://crates.io/crates/quote) crate for Rust code generation. 13 | 14 | ### Example Usage: 15 | 16 | Generate a Typescript string using `ts_string!`: 17 | 18 | ```rust 19 | let ts: String = ts_string! { const foo: number = 42; } 20 | // the value of ts is "const foo: number = 42;" 21 | ``` 22 | 23 | ### Embedding values from Rust: 24 | 25 | It's also possible to embed runtime values from Rust. 26 | 27 | This should feel familiar to anyone who has used `quote` to generate Rust code: 28 | 29 | ```rust 30 | let name = "foo"; 31 | let value: u32 = 7; 32 | 33 | let ts: String = ts_string! { const #name: number = #{value + 1}; } 34 | // the value of ts is "const foo: number = 8;" 35 | ``` 36 | 37 | Values can be included from Rust by prefixing with `#`. 38 | 39 | To include a simple value, the pattern `#` will be replaced with the value of ``. 40 | 41 | ### Literal strings: 42 | 43 | Sometimes it's not posible to represent TypeScript syntax as a valid Rust TokenStream. 44 | 45 | For instance, if we try to use `ts_string` like so it will fail: 46 | 47 | ```rust 48 | let ts: String = ts_string! { const text = 'some text here'; } 49 | let ts: String = ts_string! { const text = `some other text here`; } 50 | ``` 51 | 52 | This is becuase 'some text here' and `some other text here` are not valid Rust token streams, so they will cause a compiler error before the ts_string proc macro can parse them. 53 | 54 | To solve this problem, thie macros in this crate allow us to insert string literals directly into the output. 55 | 56 | So for instance we can escape the examples above like so: 57 | 58 | ```rust 59 | let ts: String = ts_string! { const text = #"'some text here'"; } 60 | println!("{}", ts); 61 | let ts: String = ts_string! { const text = #"`some other text here`:"; } 62 | println!("{}", ts); 63 | ``` 64 | 65 | This will print: 66 | 67 | ```ts 68 | const text = 'some text here'; 69 | const text = `some other text here`; 70 | ``` 71 | 72 | Substitutions are also supported inside literal strings, and raw strings can be literal strings: 73 | 74 | ``` 75 | let t = "text" 76 | let here = "here 77 | let ts: String = ts_string! { const text = r##"'some #t #{here}'"##; } 78 | println!("{}", ts); // prints: const text = 'some text here'; 79 | ``` 80 | 81 | ## Deno Iterop 82 | 83 | For interoperability with Deno, this library also provides the `to_quote!` macro. This allows for creation of a `deno_ast::ParsedSource` object: 84 | 85 | ```rust 86 | let ts: Result = ts_quote! { const foo = truel; }; 87 | ``` 88 | 89 | This crate also provides the `TSSource` convenience trait, which is implemented for `ParsedSource` (aliased as `TS`). 90 | 91 | This trait provides a method for formatting: 92 | 93 | ```rust 94 | let ts: ParsedSource = ts_quote! { const foo = truel; }; 95 | let source: anyhow::Result> = ts.formatted(None); 96 | ``` 97 | 98 | This method optionally takes a `dprint_plugin_typescript::configuration::Configuration` to control the output configuration. 99 | 100 | If None is provided, a common sense default will be used for formatting. 101 | 102 | Here's an example using a custom config: 103 | 104 | ```rust 105 | let ts: ParsedSource = ts_quote! { const foo = truel; }; 106 | let config = ConfigurationBuilder::new() 107 | .indent_width(4) 108 | .line_width(80) 109 | .prefer_hanging(true) 110 | .prefer_single_line(false) 111 | .quote_style(QuoteStyle::PreferDouble) 112 | .next_control_flow_position(NextControlFlowPosition::SameLine) 113 | .build(); 114 | let source: anyhow::Result = ts.formatted(Some(config)); 115 | ``` 116 | 117 | The above example desugars to the following: 118 | 119 | ```rust 120 | let ts: ParsedSource = ParsedSource::from_source( "const foo = truel;".to_string() ); 121 | let config = ConfigurationBuilder::new() 122 | .indent_width(4) 123 | .line_width(80) 124 | .prefer_hanging(true) 125 | .prefer_single_line(false) 126 | .quote_style(QuoteStyle::PreferDouble) 127 | .next_control_flow_position(NextControlFlowPosition::SameLine) 128 | .build(); 129 | let source: anyhow::Result = ts.formatted(Some(config)); 130 | ``` 131 | -------------------------------------------------------------------------------- /ts_quote/src/lib.rs: -------------------------------------------------------------------------------- 1 | use deno_ast::{parse_module, Diagnostic, SourceTextInfo}; 2 | use dprint_plugin_typescript::{ 3 | configuration::{Configuration, ConfigurationBuilder, NextControlFlowPosition, QuoteStyle}, 4 | format_parsed_source, 5 | }; 6 | pub use ts_quote_macros::ts_quote; 7 | pub use ts_quote_macros::ts_string; 8 | 9 | pub use deno_ast::ParsedSource as TS; 10 | 11 | /** 12 | The TSSource trait is used to add a few convenience methods to the deno_ast::ParsedSource type. 13 | **/ 14 | pub trait TSSource: Sized { 15 | /** 16 | Creates a ParsedSource instance from a string. 17 | 18 | # Arguments: 19 | 20 | * `source` - A TypeScript source string 21 | 22 | # Returns 23 | 24 | Returns a ParsedSource, or an error diagnostic if source is not valid TypeScript 25 | **/ 26 | fn from_source(source: String) -> Result; 27 | 28 | /** 29 | Returns a formatted TypeScript string. 30 | 31 | # Arguments: 32 | 33 | * `config` - Optional: a `dprint_plugin_typescript` config used for formatting the output. 34 | 35 | If no config is provided, the function will output using the default config: 36 | - `line_width`: `80` 37 | - `indent_width`: `2` 38 | - `prefer_hanging`: `true` 39 | - `prefer_single_line`: `false` 40 | - `quote_style`: `QuoteStyle::PreferSingle` 41 | - `next_control_flow_position`: `NextControlFlowPosition::SameLine` 42 | 43 | # Returns 44 | 45 | Returns a ParsedSource, or an error diagnostic if source is not valid TypeScript 46 | **/ 47 | fn formatted(&self, config: Option<&Configuration>) -> anyhow::Result; 48 | } 49 | 50 | impl TSSource for TS { 51 | fn from_source(source: String) -> Result { 52 | parse_module(deno_ast::ParseParams { 53 | specifier: "".to_string(), 54 | text_info: SourceTextInfo::from_string(source), 55 | media_type: deno_ast::MediaType::TypeScript, 56 | capture_tokens: true, 57 | scope_analysis: false, 58 | maybe_syntax: None, 59 | }) 60 | } 61 | 62 | fn formatted(&self, config: Option<&Configuration>) -> anyhow::Result { 63 | match config { 64 | Some(config) => Ok(format_parsed_source(self, config)?.unwrap_or(String::new())), 65 | None => { 66 | let config = ConfigurationBuilder::new() 67 | .indent_width(2) 68 | .line_width(80) 69 | .prefer_hanging(true) 70 | .prefer_single_line(false) 71 | .quote_style(QuoteStyle::PreferSingle) 72 | .next_control_flow_position(NextControlFlowPosition::SameLine) 73 | .build(); 74 | 75 | Ok(format_parsed_source(self, &config)?.unwrap_or(String::new())) 76 | } 77 | } 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | #[test] 85 | fn test_format_source_from_string() -> anyhow::Result<()> { 86 | let ts: TS = TS::from_source("let a = 1; let b = 2;".to_string())?; 87 | 88 | let output = ts.formatted(None)?; 89 | 90 | println!("output:"); 91 | println!("{}", output); 92 | 93 | assert_eq!(output.as_str(), "let a = 1;\nlet b = 2;\n"); 94 | 95 | Ok(()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ts_quote_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ts_quote_macros" 3 | version = "0.4.0" 4 | authors = ["Spencer Kohan "] 5 | edition = "2021" 6 | description = "Proc macro implementations for ts_quote" 7 | license = "Apache-2.0" 8 | 9 | repository = "https://github.com/spencerkohan/type_reflect/tree/main/ts_quote_macros" 10 | documentation = "https://docs.rs/ts_quote_macros" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | proc-macro2 = "1.0.69" 19 | quote = "1" 20 | syn = { version = "1", features = ["full", "extra-traits"] } 21 | -------------------------------------------------------------------------------- /ts_quote_macros/README.md: -------------------------------------------------------------------------------- 1 | # TS Quote Macros 2 | 3 | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](ts_quote_macros) [![Crates.io](https://img.shields.io/crates/v/ts_quote.svg)](https://crates.io/crates/ts_quote_macros) [![Documentation](https://docs.rs/ts_quote/badge.svg)](https://docs.rs/ts_quote_macros) 4 | 5 | Procedural macro implementations for `ts_quote`. This crate is for internal use, and the macros are re-exported by the `ts_quote` crate. 6 | 7 | *This crate is part of a larger workspace, see the [monorepo README](https://github.com/spencerkohan/type_reflect) for more details* 8 | -------------------------------------------------------------------------------- /ts_quote_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | // #![allow(incomplete_features)] 2 | // #![feature(specialization)] 3 | #![macro_use] 4 | 5 | mod parsing; 6 | mod ts_quote; 7 | mod ts_string; 8 | 9 | /** 10 | ts_string is a utility macro for emitting typescript strings from rust code 11 | 12 | usage: 13 | 14 | let ts: String = ts_string!{ 15 | const x = 7; 16 | }; 17 | assert_eq!(ts, "const x = 7;".to_string()); 18 | 19 | **/ 20 | #[proc_macro] 21 | pub fn ts_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 22 | match ts_string::macro_impl(input.into()) { 23 | Err(err) => err.to_compile_error(), 24 | Ok(result) => result, 25 | } 26 | .into() 27 | } 28 | 29 | /** 30 | ts_quote is a utility macro for emitting typescript from rust code 31 | 32 | ts_quote returns a Result 33 | 34 | This is aliased to the ts_quote::TS type 35 | 36 | usage: 37 | 38 | let ts: TS = ts_quote!{ 39 | const x = 7; 40 | }?; 41 | assert_eq!(ts.formatted(None), "const x = 7;".to_string()); 42 | 43 | **/ 44 | #[proc_macro] 45 | pub fn ts_quote(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 46 | match ts_quote::macro_impl(input.into()) { 47 | Err(err) => err.to_compile_error(), 48 | Ok(result) => result, 49 | } 50 | .into() 51 | } 52 | -------------------------------------------------------------------------------- /ts_quote_macros/src/ts_quote.rs: -------------------------------------------------------------------------------- 1 | use crate::parsing::ParseContext; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | use syn::Result; 5 | 6 | pub fn macro_impl(input: TokenStream) -> Result { 7 | let mut parse_context = ParseContext::new("0".to_string(), input); 8 | parse_context.parse(); 9 | 10 | let raw_string = &parse_context.format_string(); 11 | let substitution_mappings = parse_context.substitution_mappings(); 12 | 13 | Ok(quote! { 14 | ts_quote::TS::from_source(format!(#raw_string, #substitution_mappings)) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /ts_quote_macros/src/ts_string.rs: -------------------------------------------------------------------------------- 1 | use crate::parsing::ParseContext; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | use syn::Result; 5 | 6 | pub fn macro_impl(input: TokenStream) -> Result { 7 | let mut parse_context = ParseContext::new("0".to_string(), input); 8 | parse_context.parse(); 9 | 10 | let raw_string = &parse_context.format_string(); 11 | let substitution_mappings = parse_context.substitution_mappings(); 12 | 13 | Ok(quote! { 14 | format!(#raw_string, #substitution_mappings) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /type_reflect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "type_reflect" 3 | version = "0.6.2" 4 | authors = ["Spencer Kohan "] 5 | edition = "2021" 6 | description = "Extensible runtime reflection through a Derive macro" 7 | license = "Apache-2.0" 8 | 9 | repository = "https://github.com/spencerkohan/type_reflect/tree/main/type_reflect" 10 | documentation = "https://docs.rs/type_reflect" 11 | 12 | [dependencies] 13 | type_reflect_macros = "0.5.1" 14 | ts_quote = "0.4.0" 15 | type_reflect_core = "0.5.0" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | dprint-plugin-typescript = "0.95.1" 19 | 20 | [dev-dependencies] 21 | anyhow = "1.0.75" 22 | 23 | [[example]] 24 | name = "declare_and_export" 25 | path = "examples/declare_and_export/declare_and_export.rs" 26 | -------------------------------------------------------------------------------- /type_reflect/examples/declare_and_export/declare_and_export.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use type_reflect::*; 6 | use type_reflect::{export_types, Reflect}; 7 | 8 | #[derive(Debug, Reflect, Serialize, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | struct Foos { 11 | x: f32, 12 | } 13 | 14 | // Here we declare a simple struct type with Reflect 15 | // the serde(rename_all) attribute will rename the keys to 16 | // camel case, both for the JSON representation, and for 17 | // the Zod schemas when they are exported 18 | #[derive(Debug, Reflect, Serialize, Deserialize)] 19 | #[serde(rename_all = "camelCase")] 20 | struct SDParameters { 21 | prompt: String, 22 | negative_prompt: Option, 23 | cfg_scale: f32, 24 | step_count: u32, 25 | seed: u64, 26 | images: u32, 27 | foo1: Foos, 28 | foo2: Option, 29 | results: Vec, 30 | headers: HashMap, 31 | } 32 | 33 | // Here we declare an enum wiht associated values. 34 | // The `tag` attribute is required for all enums 35 | // with associated data and in this case the `data` 36 | // tag is also required (by serde) since we have 37 | // tuple-typed enum variants 38 | #[derive(Debug, Reflect, Serialize, Deserialize)] 39 | #[serde(tag = "_case", content = "data")] 40 | enum Status { 41 | Initial, 42 | #[serde(rename_all = "camelCase")] 43 | InProgress { 44 | progress: f32, 45 | should_convert: bool, 46 | }, 47 | Complete { 48 | urls: Vec, 49 | }, 50 | Double(i32, f32), 51 | Single(i32), 52 | } 53 | 54 | // Here we have a simple enum type 55 | #[derive(Debug, Reflect, Serialize, Deserialize)] 56 | enum SimpleEnumsExample { 57 | Foo, 58 | } 59 | 60 | // type AliasedEnum = SimpleEnumsExample; 61 | 62 | // And here we have an example of a type which depends 63 | // on a declared type, rather than primitive types 64 | #[derive(Debug, Reflect, Serialize, Deserialize)] 65 | struct DependantTypeExample { 66 | foo: SimpleEnumsExample, 67 | } 68 | 69 | #[derive(Debug, Serialize, Deserialize)] 70 | struct Bar {} 71 | 72 | #[derive(Debug, Reflect, Serialize, Deserialize)] 73 | struct Foo { 74 | bar: Bar, 75 | } 76 | 77 | fn main() { 78 | // When the example is run, we export the specified 79 | // types to both a Zod target, and a Rust target 80 | export_types! { 81 | types: [ 82 | Foos, 83 | SDParameters, 84 | // SimpleEnumsExample, 85 | // Status, 86 | ], 87 | destinations: [ 88 | // TypeScript( 89 | // "./type_reflect/examples/declare_and_export/output/type_script.ts" 90 | // tab_size: 2, 91 | // ), 92 | // Zod("./type_reflect/examples/declare_and_export/output/zod.ts"), 93 | // // With a prefix arg, it's possible to add additional arbitrary 94 | // // content to the output file. So for instance this might be used 95 | // // to add extra import statements for dependencies required by the 96 | // // outputed type 97 | // Rust( 98 | // "./type_reflect/examples/declare_and_export/output/rust.rs", 99 | // prefix: r#"// We add an extra comment here"# 100 | // ), 101 | ( 102 | "./type_reflect/examples/declare_and_export/output/multi.ts", 103 | emitters: [ 104 | TypeScript(), 105 | TSValidation(), 106 | TSFormat( 107 | tab_size: 2, 108 | line_width: 80, 109 | ), 110 | ] 111 | ), 112 | ] 113 | } 114 | .unwrap(); 115 | 116 | export_types! { 117 | types: [ 118 | Foo, 119 | ], 120 | destinations: [ 121 | TypeScript( 122 | "./type_reflect/examples/declare_and_export/output/type_2.ts" 123 | prefix: "import { Bar } from './bar.ts'", 124 | tab_size: 2, 125 | 126 | ), 127 | ] 128 | } 129 | .unwrap(); 130 | } 131 | -------------------------------------------------------------------------------- /type_reflect/examples/declare_and_export/output/zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | 4 | export const SDParametersSchema = z.object({ 5 | prompt: z.string(), 6 | negativePrompt: z.string().optional(), 7 | cfgScale: z.number(), 8 | stepCount: z.number(), 9 | seed: z.number(), 10 | images: z.number(), 11 | results: z.array(z.string()), 12 | headers: z.map(z.string(), z.string()), 13 | }); 14 | 15 | export type SDParameters = z.infer; 16 | 17 | 18 | export enum SimpleEnumsExample { 19 | Foo = "Foo", 20 | } 21 | 22 | export const SimpleEnumsExampleSchema = z.enum([ 23 | SimpleEnumsExample.Foo, 24 | ]) 25 | 26 | 27 | export enum StatusCase { 28 | Initial = "Initial", 29 | InProgress = "InProgress", 30 | Complete = "Complete", 31 | Double = "Double", 32 | Single = "Single", 33 | } 34 | 35 | 36 | export const StatusCaseInitialSchema = z.object({ 37 | _case: z.literal(StatusCase.Initial), 38 | }); 39 | export type StatusCaseInitial = z.infer 40 | 41 | export const StatusCaseInProgressSchema = z.object({ 42 | _case: z.literal(StatusCase.InProgress), 43 | data: z.object({ 44 | progress: z.number(), 45 | shouldConvert: z.bool(), 46 | })}); 47 | export type StatusCaseInProgress = z.infer 48 | 49 | export const StatusCaseCompleteSchema = z.object({ 50 | _case: z.literal(StatusCase.Complete), 51 | data: z.object({ 52 | urls: z.array(z.string()), 53 | })}); 54 | export type StatusCaseComplete = z.infer 55 | 56 | export const StatusCaseDoubleSchema = z.object({ 57 | _case: z.literal(StatusCase.Double), 58 | data: z.tuple([ 59 | z.number(), 60 | z.number(), 61 | ])}); 62 | export type StatusCaseDouble = z.infer 63 | 64 | export const StatusCaseSingleSchema = z.object({ 65 | _case: z.literal(StatusCase.Single), 66 | data: z.number()}); 67 | export type StatusCaseSingle = z.infer 68 | 69 | 70 | export const StatusSchema = z.union([ 71 | StatusCaseInitialSchema, 72 | StatusCaseInProgressSchema, 73 | StatusCaseCompleteSchema, 74 | StatusCaseDoubleSchema, 75 | StatusCaseSingleSchema, 76 | ]); 77 | export type Status = z.infer 78 | 79 | -------------------------------------------------------------------------------- /type_reflect/src/alias_type.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::type_description::Type; 2 | 3 | /// A type implementing `AliasType` can 4 | /// be used to emit a type alias representation 5 | pub trait AliasType { 6 | fn name() -> &'static str; 7 | fn source_type() -> Type; 8 | fn rust() -> String; 9 | } 10 | -------------------------------------------------------------------------------- /type_reflect/src/enum_type.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::{ 2 | type_description::{EnumCase, EnumType}, 3 | Inflection, 4 | }; 5 | 6 | /// A type implementing `EnumReflectionType` can 7 | /// be used to emit a enum representation 8 | pub trait EnumReflectionType { 9 | fn name() -> &'static str; 10 | fn inflection() -> Inflection; 11 | fn cases() -> Vec; 12 | fn enum_type() -> EnumType; 13 | fn rust() -> String; 14 | } 15 | -------------------------------------------------------------------------------- /type_reflect/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(incomplete_features)] 2 | #![feature(specialization)] 3 | #![feature(let_chains)] 4 | pub use core::convert::AsRef; 5 | use std::ffi::OsStr; 6 | use type_reflect_macros; 7 | 8 | use std::fs::File; 9 | pub use std::io::Write; 10 | pub use std::path::Path; 11 | 12 | pub use ts_quote::ts_string; 13 | pub use type_reflect_macros::export_types; 14 | pub use type_reflect_macros::Reflect; 15 | pub mod struct_type; 16 | pub use struct_type::*; 17 | pub use type_reflect_core::*; 18 | pub mod zod; 19 | pub use zod::Zod; 20 | pub mod rust; 21 | pub use rust::Rust; 22 | pub mod enum_type; 23 | pub use enum_type::*; 24 | pub mod alias_type; 25 | pub use alias_type::*; 26 | 27 | pub mod type_script; 28 | pub use type_script::TypeScript; 29 | 30 | pub mod ts_validation; 31 | pub use ts_validation::TSValidation; 32 | 33 | pub mod ts_format; 34 | pub use ts_format::TSFormat; 35 | 36 | pub use serde::{Deserialize, Serialize}; 37 | pub use serde_json; 38 | 39 | /// Any type implementing the `Emittable` trait 40 | /// can be used with a `TypeEmitter` to generate 41 | /// the target representation. 42 | /// 43 | /// Generally the `Emittable` trait implementation 44 | /// will be generated by the `Reflect` derive macro. 45 | pub trait Emittable { 46 | fn emit_with(emitter: &mut E) -> String; 47 | } 48 | 49 | /// init_destination_file is called to generate the target file 50 | /// 51 | /// Args: 52 | /// - path: the path at which the file should be created, 53 | /// relative to the current working directory at runtime 54 | /// - prefix: a prefix which will be added to the output file, 55 | /// for instance if it's needed to inject additional imports 56 | /// to make the target work 57 | pub fn init_destination_file( 58 | path: P, 59 | prefix: Pref, 60 | ) -> Result 61 | where 62 | P: AsRef, 63 | Pref: AsRef<[u8]>, 64 | { 65 | let mut file = match File::create(path.clone()) { 66 | Ok(file) => file, 67 | Err(err) => { 68 | eprintln!("Error creating file: {:?}", path); 69 | return Err(err); 70 | } 71 | }; 72 | file.write_all(prefix.as_ref())?; 73 | Ok(file) 74 | } 75 | 76 | pub fn write_postfix( 77 | path: P, 78 | postfix: Post, 79 | ) -> Result<(), std::io::Error> 80 | where 81 | P: AsRef, 82 | Post: AsRef<[u8]>, 83 | { 84 | let mut file = std::fs::OpenOptions::new() 85 | .write(true) 86 | .append(true) 87 | .open(path)?; 88 | file.write_all(postfix.as_ref())?; 89 | Ok(()) 90 | } 91 | 92 | /// The `TypeEmitter` trait defines how an `Emittable` can be used 93 | /// to generate the desired target representation. 94 | /// 95 | /// So for example, the Zod component defines how a type representation, 96 | /// as generated by the `Reflect` derive macro, will be translated into 97 | /// the Zed representation 98 | pub trait TypeEmitter { 99 | // /// init_destination_file is called to generate the target file 100 | // /// 101 | // /// Args: 102 | // /// - path: the path at which the file should be created, 103 | // /// relative to the current working directory at runtime 104 | // /// - prefix: a prefix which will be added to the output file, 105 | // /// for instance if it's needed to inject additional imports 106 | // /// to make the target work 107 | // fn init_destination_file( 108 | // &mut self, 109 | // path: P, 110 | // prefix: Pref, 111 | // ) -> Result 112 | // where 113 | // P: AsRef, 114 | // Pref: AsRef<[u8]>, 115 | // { 116 | // let mut file = match File::create(path.clone()) { 117 | // Ok(file) => file, 118 | // Err(err) => { 119 | // eprintln!("Error creating file: {:?}", path); 120 | // return Err(err); 121 | // } 122 | // }; 123 | // file.write_all(prefix.as_ref())?; 124 | // file.write_all(self.prefix().as_bytes())?; 125 | // file.write_all("\n".as_ref())?; 126 | // Ok(file) 127 | // } 128 | 129 | /// finalize is called after all the types have been 130 | /// emitted into the destination file 131 | /// 132 | /// Here additional cleanup or post-procesing can be done, 133 | /// for example linting or code-formatting 134 | fn finalize

(&mut self, path: P) -> Result<(), std::io::Error> 135 | where 136 | P: AsRef; 137 | 138 | /// The prefix method generates text for a prefix 139 | /// which will be prefixed to the target file. 140 | /// 141 | /// This would, for example, be the place to provide imports 142 | /// required for the target language or framework. 143 | fn prefix(&mut self) -> String; 144 | fn emit(&mut self) -> String 145 | where 146 | Self: Sized, 147 | { 148 | T::emit_with::(self) 149 | } 150 | 151 | /// Emit a struct representation from a struct type 152 | fn emit_struct(&mut self) -> String 153 | where 154 | T: StructType; 155 | 156 | /// Emit an enum representation from an enum type 157 | fn emit_enum(&mut self) -> String 158 | where 159 | T: EnumReflectionType; 160 | 161 | /// Emit a type-alias representation from an alias type 162 | fn emit_alias(&mut self) -> String 163 | where 164 | T: AliasType; 165 | } 166 | 167 | pub trait RustType { 168 | fn emit_rust(&self) -> String; 169 | } 170 | -------------------------------------------------------------------------------- /type_reflect/src/rust.rs: -------------------------------------------------------------------------------- 1 | pub use super::struct_type::*; 2 | pub use super::type_description::Type; 3 | pub use super::*; 4 | use std::ffi::OsStr; 5 | use std::process::Command; 6 | 7 | #[derive(Default)] 8 | pub struct Rust {} 9 | 10 | const DERIVES: &str = "#[derive(Debug, Clone, Serialize, Deserialize)]"; 11 | 12 | impl TypeEmitter for Rust { 13 | fn prefix(&mut self) -> String { 14 | "use serde::{Deserialize, Serialize};\nuse serde_json;\n".to_string() 15 | } 16 | 17 | fn emit_struct(&mut self) -> String 18 | where 19 | T: StructType, 20 | { 21 | format!("\n{}\n{}\n", DERIVES, T::rust()) 22 | } 23 | 24 | fn emit_enum(&mut self) -> String 25 | where 26 | T: EnumReflectionType, 27 | { 28 | format!("\n{}\n{}\n", DERIVES, T::rust()) 29 | } 30 | 31 | fn emit_alias(&mut self) -> String 32 | where 33 | T: AliasType, 34 | { 35 | format!("\n{}\n{}\n", DERIVES, T::rust()) 36 | } 37 | 38 | fn finalize

(&mut self, path: P) -> Result<(), std::io::Error> 39 | where 40 | P: AsRef, 41 | { 42 | let output = Command::new("rustfmt").arg(path).output()?; 43 | if !output.status.success() { 44 | eprintln!("Failed to format file"); 45 | eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr)); 46 | } 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /type_reflect/src/struct_type.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::{type_description::TypeFieldsDefinition, Inflection}; 2 | 3 | /// A type implementing `StructType` can 4 | /// be used to emit a struct representation 5 | pub trait StructType { 6 | fn name() -> &'static str; 7 | fn inflection() -> Inflection; 8 | fn fields() -> TypeFieldsDefinition; 9 | fn rust() -> String; 10 | } 11 | -------------------------------------------------------------------------------- /type_reflect/src/ts_format.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsStr, path::Path}; 2 | 3 | use dprint_plugin_typescript::{ 4 | configuration::{ 5 | ConfigurationBuilder, NextControlFlowPosition, PreferHanging, QuoteStyle, 6 | SameOrNextLinePosition, 7 | }, 8 | FormatTextOptions, 9 | }; 10 | 11 | use crate::{AliasType, EnumReflectionType, StructType, TypeEmitter}; 12 | 13 | pub struct TSFormat { 14 | pub tab_size: u8, 15 | pub line_width: u32, 16 | } 17 | 18 | impl Default for TSFormat { 19 | fn default() -> Self { 20 | Self { 21 | tab_size: 2, 22 | line_width: 80, 23 | } 24 | } 25 | } 26 | 27 | impl TypeEmitter for TSFormat { 28 | fn prefix(&mut self) -> String { 29 | "".to_string() 30 | } 31 | 32 | fn emit_struct(&mut self) -> String 33 | where 34 | T: StructType, 35 | { 36 | "".to_string() 37 | } 38 | 39 | fn emit_enum(&mut self) -> String 40 | where 41 | T: EnumReflectionType, 42 | { 43 | "".to_string() 44 | } 45 | 46 | fn emit_alias(&mut self) -> String 47 | where 48 | T: AliasType, 49 | { 50 | "".to_string() 51 | } 52 | 53 | fn finalize

(&mut self, path: P) -> Result<(), std::io::Error> 54 | where 55 | P: AsRef, 56 | { 57 | // build the configuration once 58 | let config = ConfigurationBuilder::new() 59 | .indent_width(self.tab_size) 60 | .line_width(self.line_width) 61 | .build(); 62 | 63 | let file_path = Path::new(&path); 64 | 65 | let text: String = std::fs::read_to_string(Path::new(&path))?; 66 | 67 | let options: FormatTextOptions = FormatTextOptions { 68 | path: Path::new(&path), 69 | extension: None, 70 | text, 71 | config: &config, 72 | external_formatter: None, 73 | }; 74 | 75 | let result = dprint_plugin_typescript::format_text(options); 76 | 77 | match result { 78 | Ok(Some(contents)) => { 79 | std::fs::write(file_path, contents)?; 80 | } 81 | Err(err) => { 82 | eprintln!("Error formatting typescript: {}", err); 83 | } 84 | _ => { 85 | eprintln!("Failed to format text: no output generated"); 86 | } 87 | }; 88 | 89 | Ok(()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/enum_type/case_type.rs: -------------------------------------------------------------------------------- 1 | use ts_quote::ts_string; 2 | use type_reflect_core::{EnumCase, Inflection, NamedField, Type}; 3 | 4 | use crate::ts_validation::{ 5 | struct_type::named_field_validations, validation::tuple_validation, validation_namespace, 6 | }; 7 | 8 | pub fn emit_complex_enum_case_type( 9 | enum_name: &str, 10 | case_key: &String, 11 | content_key: &Option, 12 | case: EnumCase, 13 | ) -> String { 14 | let case_key_value: String = format!("{}CaseKey.{}", enum_name, case.name); 15 | let case_type_name: String = format!("{}Case{}", enum_name, case.name); 16 | 17 | let validator = match &case.type_ { 18 | type_reflect_core::TypeFieldsDefinition::Unit => emit_simple_case_type_validator(), 19 | type_reflect_core::TypeFieldsDefinition::Tuple(members) => { 20 | emit_tuple_case_type_validator(content_key, &members) 21 | } 22 | type_reflect_core::TypeFieldsDefinition::Named(members) => { 23 | emit_struct_case_type_validator(content_key, &members, case.inflection) 24 | } 25 | }; 26 | 27 | let validation_impl = match &case.type_ { 28 | type_reflect_core::TypeFieldsDefinition::Unit => { 29 | ts_string! { 30 | if (!isRecord(input)) { 31 | throw new Error(#"`Error parsing #case_type_name: expected: Record, found: ${typeof input}`"); 32 | } 33 | if (input.#case_key !== #case_key_value) { 34 | throw new Error(#"`Error parsing #case_type_name: expected key: #case_key_value, found: ${typeof input}`"); 35 | } 36 | return input as #case_type_name 37 | } 38 | } 39 | _ => { 40 | ts_string! { 41 | if (!isRecord(input)) { 42 | throw new Error(#"`Error parsing #case_type_name: expected: Record, found: ${typeof input}`"); 43 | } 44 | if (input.#case_key !== #case_key_value) { 45 | throw new Error(#"`Error parsing #case_type_name: expected key: #case_key_value, found: ${typeof input}`"); 46 | } 47 | #validator 48 | return input as #case_type_name 49 | } 50 | } 51 | }; 52 | 53 | return validation_namespace(case_type_name.as_str(), validation_impl.as_str()); 54 | } 55 | 56 | fn emit_simple_case_type_validator() -> String { 57 | String::new() 58 | } 59 | 60 | fn emit_struct_case_type_validator( 61 | content_key: &Option, 62 | members: &Vec, 63 | inflection: Inflection, 64 | ) -> String { 65 | let member_prefix = match content_key { 66 | None => "input".to_string(), 67 | Some(key) => format!("input.{}", key), 68 | }; 69 | named_field_validations(member_prefix.as_str(), members, inflection) 70 | } 71 | 72 | fn emit_tuple_case_type_validator(content_key: &Option, members: &Vec) -> String { 73 | let member_prefix = match content_key { 74 | None => "input".to_string(), 75 | Some(key) => format!("input.{}", key), 76 | }; 77 | tuple_validation(member_prefix.as_str(), members) 78 | } 79 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/enum_type/complex.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::EnumCase; 2 | 3 | use crate::{ts_validation::validation_namespace, EnumReflectionType}; 4 | 5 | use super::case_type::emit_complex_enum_case_type; 6 | use ts_quote::ts_string; 7 | 8 | pub fn emit_complex_enum_type(case_key: &String, content_key: &Option) -> String 9 | where 10 | T: EnumReflectionType, 11 | { 12 | let case_type_validators: String = T::cases() 13 | .into_iter() 14 | .map(|case: EnumCase| emit_complex_enum_case_type(T::name(), case_key, content_key, case)) 15 | .collect(); 16 | 17 | let case_validations: String = T::cases() 18 | .into_iter() 19 | .map(|case: EnumCase| validate_case(T::name(), &case)) 20 | .collect(); 21 | 22 | let name = T::name(); 23 | 24 | let namespace = validation_namespace(T::name(), ts_string! { 25 | #case_validations 26 | throw new Error(#"`Error validating #name: value ${JSON.stringify(input)} does not match any variant`"); 27 | }.as_str()); 28 | 29 | ts_string! { 30 | #case_type_validators 31 | #namespace 32 | } 33 | } 34 | 35 | fn validate_case(type_name: &str, case: &EnumCase) -> String { 36 | let case_type = format!("{}Case{}", type_name, case.name); 37 | 38 | ts_string! { 39 | try { 40 | return #case_type.validate(input); 41 | } catch {} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/enum_type/mod.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::EnumType; 2 | use untagged::emit_untagged_enum_type; 3 | 4 | use crate::{ts_validation::validation_namespace, EnumReflectionType}; 5 | 6 | mod complex; 7 | use complex::*; 8 | 9 | mod case_type; 10 | 11 | mod untagged; 12 | 13 | pub fn emit_enum_type() -> String 14 | where 15 | T: EnumReflectionType, 16 | { 17 | match T::enum_type() { 18 | EnumType::Simple => emit_simple_enum_type::(), 19 | EnumType::Complex { 20 | case_key, 21 | content_key, 22 | } => emit_complex_enum_type::(&case_key, &content_key), 23 | EnumType::Untagged => emit_untagged_enum_type::(), 24 | } 25 | } 26 | 27 | fn emit_simple_enum_type() -> String 28 | where 29 | T: EnumReflectionType, 30 | { 31 | let validation_impl = format!( 32 | r#" 33 | if(Object.values({name}).includes(input as {name})) {{ 34 | return input as {name}; 35 | }} 36 | throw new Error(`Error parsing {name}: value does not conform: ${{JSON.stringify(input)}}`) 37 | "#, 38 | name = T::name(), 39 | ); 40 | validation_namespace(T::name(), validation_impl.as_str()) 41 | } 42 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/enum_type/untagged/case_type.rs: -------------------------------------------------------------------------------- 1 | use ts_quote::ts_string; 2 | use type_reflect_core::{EnumCase, Type}; 3 | 4 | use crate::ts_validation::{ 5 | struct_type::named_field_validations, validation::tuple_validation, validation_namespace, 6 | }; 7 | 8 | pub fn emit_case_type(case: &EnumCase, parent_name: &str) -> String { 9 | let case_type = format!("{}Case{}", parent_name, case.name); 10 | let validation_impl = match &case.type_ { 11 | type_reflect_core::TypeFieldsDefinition::Unit => { 12 | unreachable!("Unit cases don't emit case types"); 13 | } 14 | type_reflect_core::TypeFieldsDefinition::Tuple(members) => { 15 | if members.len() == 1 { 16 | return "".to_string(); 17 | } 18 | tuple_validation("input", &members) 19 | } 20 | type_reflect_core::TypeFieldsDefinition::Named(fields) => { 21 | let val = named_field_validations("input", &fields, case.inflection); 22 | ts_string! { 23 | if (!isRecord(input)) { 24 | throw new Error(#r#"`Error parsing #case_type: expected: Record, found: ${typeof input}`"#); 25 | } 26 | #val 27 | } 28 | } 29 | }; 30 | 31 | let validation_impl = ts_string! { 32 | #validation_impl 33 | return input as #case_type; 34 | }; 35 | 36 | validation_namespace(&case_type, &validation_impl) 37 | } 38 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/enum_type/untagged/mod.rs: -------------------------------------------------------------------------------- 1 | use case_type::emit_case_type; 2 | use ts_quote::ts_string; 3 | use type_reflect_core::{EnumCase, Inflectable, TypeFieldsDefinition}; 4 | use union_case::union_case_validation; 5 | use unit_case::unit_case_validation; 6 | 7 | use crate::{ts_validation::validation_namespace, EnumReflectionType}; 8 | mod case_type; 9 | mod union_case; 10 | mod unit_case; 11 | 12 | pub fn emit_untagged_enum_type() -> String 13 | where 14 | T: EnumReflectionType, 15 | { 16 | let name = T::name(); 17 | let cases = T::cases(); 18 | let inflection = T::inflection(); 19 | 20 | let unit_cases: Vec = cases 21 | .iter() 22 | .filter(|c| { 23 | if let TypeFieldsDefinition::Unit = c.type_ { 24 | true 25 | } else { 26 | false 27 | } 28 | }) 29 | .map(|case| { 30 | let name = case.name.inflect(inflection); 31 | name 32 | }) 33 | .collect(); 34 | 35 | let unit_case_validations = if unit_cases.is_empty() { 36 | "".to_string() 37 | } else { 38 | let unit_case_validations: Vec<_> = unit_cases 39 | .into_iter() 40 | .map(|case_name| unit_case_validation(case_name.as_str(), &name)) 41 | .collect(); 42 | let unit_case_validations = unit_case_validations.join("\n"); 43 | ts_string! { 44 | if (#"'string'" === typeof input) { 45 | #unit_case_validations 46 | throw new Error(#"`Error validating #name: none of the unit cases were matched`"); 47 | } 48 | } 49 | }; 50 | 51 | let union_cases: Vec<&EnumCase> = cases 52 | .iter() 53 | .filter(|c| { 54 | if let TypeFieldsDefinition::Unit = c.type_ { 55 | false 56 | } else { 57 | true 58 | } 59 | }) 60 | .collect(); 61 | 62 | let union_case_validations = if union_cases.is_empty() { 63 | "".to_string() 64 | } else { 65 | let union_case_validations: Vec<_> = union_cases 66 | .iter() 67 | .map(|case| union_case_validation(case, &name, inflection)) 68 | .collect(); 69 | let union_case_validations = union_case_validations.join("\n"); 70 | ts_string! { 71 | if (!isRecord(input)) { 72 | throw new Error(#r#"`Error parsing #name: expected: Record, found: ${typeof input}`"#); 73 | } 74 | #union_case_validations 75 | } 76 | }; 77 | 78 | let union_case_types: Vec<_> = union_cases 79 | .iter() 80 | .map(|case| emit_case_type(case, &name)) 81 | .collect(); 82 | 83 | let union_case_types = union_case_types.join("\n"); 84 | 85 | let namespace = validation_namespace( 86 | &name, 87 | &ts_string! { 88 | #unit_case_validations 89 | #union_case_validations 90 | throw new Error(#"`Error validating #name: none of the union cases were matched`"); 91 | }, 92 | ); 93 | 94 | ts_string! { 95 | 96 | #union_case_types 97 | #namespace 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/enum_type/untagged/union_case.rs: -------------------------------------------------------------------------------- 1 | use ts_quote::ts_string; 2 | use type_reflect_core::{inflection, EnumCase, Inflectable, Inflection, Type}; 3 | 4 | use crate::{ 5 | ts_validation::validation::type_validation, 6 | type_script::untagged_enum_type::emit_case_type_name, 7 | }; 8 | 9 | pub fn union_case_validation(case: &EnumCase, parent_name: &str, inflection: Inflection) -> String { 10 | let case_key = case.name.inflect(inflection); 11 | let case_type_name = emit_case_type_name(case, parent_name); 12 | 13 | let case_validation = match &case.type_ { 14 | type_reflect_core::TypeFieldsDefinition::Unit => { 15 | unreachable!("Unit cases are handled separately"); 16 | } 17 | type_reflect_core::TypeFieldsDefinition::Tuple(items) => { 18 | validate_tuple_case(case, &items, parent_name, &case_key) 19 | } 20 | type_reflect_core::TypeFieldsDefinition::Named(_) => { 21 | validate_struct_case(case, parent_name, &case_key) 22 | } 23 | }; 24 | 25 | ts_string! { 26 | if (input.#case_key) { 27 | #case_validation 28 | } 29 | } 30 | } 31 | 32 | fn validate_tuple_case( 33 | case: &EnumCase, 34 | tuple_members: &Vec, 35 | parent_name: &str, 36 | case_key: &str, 37 | ) -> String { 38 | if tuple_members.len() == 1 { 39 | let Some(case_type) = tuple_members.first() else { 40 | return "_ERROR_NO_CASE_TYPE_EXISTS_".to_string(); 41 | }; 42 | let var_name = ts_string! { input.#case_key }; 43 | let val = type_validation(&var_name, case_type); 44 | ts_string! { 45 | #val 46 | return input as #parent_name; 47 | } 48 | } else { 49 | let case_type = format!("{}Case{}", parent_name, case.name); 50 | ts_string! { 51 | return { #case_key: #case_type.validate(input.#case_key) }; 52 | } 53 | } 54 | } 55 | 56 | fn validate_struct_case(case: &EnumCase, parent_name: &str, case_key: &str) -> String { 57 | let case_type = format!("{}Case{}", parent_name, case.name); 58 | ts_string! { 59 | return { #case_key: #case_type.validate(input.#case_key) }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/enum_type/untagged/unit_case.rs: -------------------------------------------------------------------------------- 1 | use ts_quote::ts_string; 2 | 3 | pub fn unit_case_validation(case_name: &str, type_name: &str) -> String { 4 | ts_string! { 5 | if (input === #"'#case_name'") { 6 | return input as #type_name 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | use crate::{AliasType, EnumReflectionType, StructType, TypeEmitter}; 4 | 5 | mod struct_type; 6 | use struct_type::struct_impl; 7 | 8 | mod enum_type; 9 | use enum_type::emit_enum_type; 10 | use ts_quote::ts_string; 11 | 12 | mod validation; 13 | 14 | #[derive(Default)] 15 | pub struct TSValidation {} 16 | 17 | impl TypeEmitter for TSValidation { 18 | fn prefix(&mut self) -> String { 19 | r#" 20 | function isRecord(value: any): value is Record { 21 | return typeof value === 'object' && value !== null && !Array.isArray(value); 22 | } 23 | "# 24 | .to_string() 25 | } 26 | 27 | fn emit_struct(&mut self) -> String 28 | where 29 | T: StructType, 30 | { 31 | let name = T::name(); 32 | struct_impl(&name, &T::fields(), T::inflection()) 33 | } 34 | 35 | fn emit_enum(&mut self) -> String 36 | where 37 | T: EnumReflectionType, 38 | { 39 | emit_enum_type::() 40 | } 41 | 42 | fn emit_alias(&mut self) -> String 43 | where 44 | T: AliasType, 45 | { 46 | "".to_string() 47 | } 48 | 49 | fn finalize

(&mut self, _path: P) -> Result<(), std::io::Error> 50 | where 51 | P: AsRef, 52 | { 53 | Ok(()) 54 | } 55 | } 56 | 57 | pub fn validation_namespace(name: &str, validation_impl: &str) -> String { 58 | ts_string! { 59 | export namespace #name { 60 | export function validate(input: any): #name { 61 | #validation_impl 62 | } 63 | 64 | export function parse(input: string): #name { 65 | let json = JSON.parse(input); 66 | return validate(json); 67 | } 68 | 69 | export function tryValidate(input: any): #name | undefined { 70 | try { 71 | return validate(input); 72 | } catch { 73 | return undefined; 74 | } 75 | } 76 | 77 | export function tryParse(input: string): #name | undefined { 78 | let json = JSON.parse(input); 79 | return tryValidate(json); 80 | } 81 | 82 | export function validateArray(input: any): Array<#name> { 83 | if (!Array.isArray(input)) { 84 | throw new Error(#"`Error validating Array<#name>: expected: Array, found: ${ typeof input }`"); 85 | } 86 | for (const item of input) { 87 | validate(item); 88 | } 89 | return input as Array<#name>; 90 | } 91 | 92 | export function parseArray(input: string): Array<#name> { 93 | let json = JSON.parse(input); 94 | return validateArray(json); 95 | } 96 | 97 | export function tryValidateArray(input: any): Array<#name> | undefined { 98 | try { 99 | return validateArray(input); 100 | } catch (e: any) { 101 | return undefined; 102 | } 103 | } 104 | 105 | export function tryParseArray(input: any): Array<#name> | undefined { 106 | try { 107 | return parseArray(input); 108 | } catch (e: any) { 109 | return undefined; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/struct_type.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::{Inflectable, Inflection, NamedField, TypeFieldsDefinition}; 2 | 3 | use super::{ 4 | validation::{tuple_validation, type_validation}, 5 | validation_namespace, 6 | }; 7 | use ts_quote::*; 8 | 9 | pub fn named_field_validations( 10 | member_prefix: &str, 11 | members: &Vec, 12 | inflection: Inflection, 13 | ) -> String { 14 | let members: Vec = members 15 | .into_iter() 16 | .map(|member| { 17 | let member_name = member.name.inflect(inflection); 18 | type_validation( 19 | ts_string! { 20 | #{member_prefix}.#{member_name} 21 | } 22 | .as_str(), 23 | &member.type_, 24 | ) 25 | 26 | // type_validation( 27 | // format!("{}.{}", member_prefix, member_name).as_str(), 28 | // &member.type_, 29 | // ) 30 | }) 31 | .collect(); 32 | members.join("\n ") 33 | } 34 | 35 | pub fn struct_field_validations( 36 | member_prefix: &str, 37 | fields: &TypeFieldsDefinition, 38 | inflection: Inflection, 39 | ) -> String { 40 | match fields { 41 | TypeFieldsDefinition::Unit => todo!(), 42 | TypeFieldsDefinition::Tuple(tuple) => tuple_validation(member_prefix, tuple), 43 | TypeFieldsDefinition::Named(named) => { 44 | named_field_validations(member_prefix, named, inflection) 45 | } 46 | } 47 | } 48 | 49 | pub fn struct_impl(name: &str, fields: &TypeFieldsDefinition, inflection: Inflection) -> String { 50 | let validations = struct_field_validations("input", fields, inflection); 51 | 52 | let validation_impl = match fields { 53 | TypeFieldsDefinition::Unit => todo!(), 54 | TypeFieldsDefinition::Tuple(_) => { 55 | ts_string! { 56 | #validations 57 | return input as #name; 58 | } 59 | } 60 | TypeFieldsDefinition::Named(_) => ts_string! { 61 | if (!isRecord(input)) { 62 | throw new Error(#r#"`Error parsing #name#: expected: Record, found: ${typeof input}`"#); 63 | } 64 | #validations 65 | return input as #name; 66 | }, 67 | }; 68 | 69 | // let validation_impl = format!( 70 | // r#" 71 | // if (!isRecord(input)) {{ 72 | // throw new Error(`Error parsing {name}: expected: Record, found: ${{typeof input}}`); 73 | // }} 74 | // {validations} 75 | // return input as {name}; 76 | // "#, 77 | // name = name, 78 | // validations = validations 79 | // ); 80 | 81 | validation_namespace(name, validation_impl.as_str()) 82 | } 83 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/validation/array.rs: -------------------------------------------------------------------------------- 1 | use ts_quote::ts_string; 2 | use type_reflect_core::Type; 3 | 4 | use crate::ts_validation::validation::type_validation; 5 | 6 | pub fn array_validation(var_name: &str, member_type: &Type) -> String { 7 | let validation = type_validation("item", member_type); 8 | ts_string! { 9 | if (!Array.isArray(#var_name)) { 10 | throw new Error(#"`Error parsing #var_name: expected: Array, found: ${ typeof #var_name }`"); 11 | } 12 | for (const item of #var_name) { 13 | #validation 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/validation/map.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::Type; 2 | 3 | use crate::ts_validation::validation::type_validation; 4 | 5 | pub fn map_validation(var_name: &str, member_type: &Type) -> String { 6 | let validation = type_validation("item", member_type); 7 | format!( 8 | r#" 9 | if (!isRecord({var_name})) {{ 10 | throw new Error(`Error parsing {var_name}: expected: Record, found: ${{ typeof {var_name} }}`); 11 | }} 12 | for (const key in {var_name}) {{ 13 | const item = {var_name}[key]; 14 | {validation} 15 | }} 16 | "#, 17 | var_name = var_name, 18 | validation = validation, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/validation/mod.rs: -------------------------------------------------------------------------------- 1 | mod type_validation; 2 | pub use type_validation::*; 3 | 4 | mod primitive; 5 | pub use primitive::*; 6 | 7 | mod array; 8 | pub use array::*; 9 | 10 | mod map; 11 | 12 | mod tuple; 13 | pub use tuple::*; 14 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/validation/primitive.rs: -------------------------------------------------------------------------------- 1 | pub fn primitive_type_validation(var_name: &str, primitive_type: &str) -> String { 2 | format!( 3 | r#" 4 | if ('{primitive_type}' !== typeof {var_name}) {{ 5 | throw new Error(`Validation error: expected: {primitive_type}, found: ${{ typeof {var_name} }}`); 6 | }} 7 | "#, 8 | var_name = var_name, 9 | primitive_type = primitive_type 10 | ) 11 | } 12 | 13 | // Better error? 14 | // throw new Error(`Error parsing {parent_name}.{name}: expected: {primitive}, found: ${{ typeof input.{name} }}`); 15 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/validation/tuple.rs: -------------------------------------------------------------------------------- 1 | use crate::ts_validation::validation::type_validation; 2 | use ts_quote::ts_string; 3 | use type_reflect_core::Type; 4 | 5 | pub fn tuple_validation(var_name: &str, member_types: &Vec) -> String { 6 | if member_types.len() == 1 { 7 | return type_validation(var_name, &member_types[0]); 8 | } 9 | 10 | let member_validations: String = member_types 11 | .into_iter() 12 | .enumerate() 13 | .map(|(i, member)| { 14 | type_validation( 15 | ts_string! { 16 | #var_name[#i] 17 | } 18 | .as_str(), 19 | &member, 20 | ) 21 | }) 22 | .collect(); 23 | 24 | ts_string! { 25 | if (!Array.isArray(#var_name)) { 26 | throw new Error(#"`Error parsing #var_name: expected: Array, found: ${ typeof #var_name }`"); 27 | } 28 | #member_validations 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /type_reflect/src/ts_validation/validation/type_validation.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::Type; 2 | 3 | use crate::type_script::to_ts_type; 4 | 5 | use super::{array_validation, map::map_validation, primitive_type_validation}; 6 | 7 | pub fn type_validation(var_name: &str, type_: &Type) -> String { 8 | match type_ { 9 | Type::String => primitive_type_validation(var_name, "string"), 10 | Type::Float | Type::Int | Type::UnsignedInt => { 11 | primitive_type_validation(var_name, "number") 12 | } 13 | Type::Boolean => primitive_type_validation(var_name, "boolean"), 14 | Type::Array(t) => array_validation(var_name, &t), 15 | Type::Map { key: _, value } => map_validation(var_name, value), 16 | Type::Option(t) => { 17 | let type_validation = type_validation(var_name, &t); 18 | format!( 19 | r#" 20 | if ({var_name}) {{ 21 | {type_validation} 22 | }} 23 | "#, 24 | var_name = var_name, 25 | type_validation = type_validation 26 | ) 27 | } 28 | Type::Named(_) => { 29 | let value_type = to_ts_type(type_); 30 | format!( 31 | r#" 32 | {value_type}.validate({var_name}) 33 | "#, 34 | var_name = var_name, 35 | value_type = value_type 36 | ) 37 | } 38 | Type::Transparent(type_) => type_validation(var_name, &*(type_.type_)), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /type_reflect/src/type_script/alias_type.rs: -------------------------------------------------------------------------------- 1 | use crate::AliasType; 2 | 3 | use super::to_ts_type; 4 | 5 | pub fn emit_alias_type() -> String 6 | where 7 | T: AliasType, 8 | { 9 | return format!( 10 | r#" 11 | 12 | export type {alias} = {source}; 13 | 14 | "#, 15 | alias = T::name(), 16 | source = to_ts_type(&T::source_type()) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /type_reflect/src/type_script/enum_type.rs: -------------------------------------------------------------------------------- 1 | use ts_quote::ts_string; 2 | use type_reflect_core::{EnumCase, EnumType, Inflectable, Inflection}; 3 | 4 | use super::untagged_enum_type::emit_untaggedd_enum_type; 5 | use crate::type_script::type_fields; 6 | use crate::EnumReflectionType; 7 | 8 | use super::to_ts_type; 9 | 10 | pub fn emit_enum_type() -> String 11 | where 12 | T: EnumReflectionType, 13 | { 14 | match T::enum_type() { 15 | EnumType::Simple => emit_simple_enum_type::(), 16 | EnumType::Complex { 17 | case_key, 18 | content_key, 19 | } => emit_complex_enum_type::(&case_key, &content_key), 20 | EnumType::Untagged => emit_untaggedd_enum_type::(), 21 | } 22 | } 23 | 24 | fn emit_simple_enum_type() -> String 25 | where 26 | T: EnumReflectionType, 27 | { 28 | let inflection = T::inflection(); 29 | let simple_cases: String = T::cases() 30 | .into_iter() 31 | .map(|case| { 32 | let inflected = case.name.inflect(inflection); 33 | format!( 34 | r#" {name} = "{inflected}", 35 | "#, 36 | name = case.name 37 | ) 38 | }) 39 | .collect(); 40 | 41 | format!( 42 | r#" 43 | export enum {name} {{ 44 | {simple_cases}}} 45 | "#, 46 | name = T::name(), 47 | simple_cases = simple_cases, 48 | ) 49 | } 50 | 51 | fn emit_complex_enum_type(case_key: &String, content_key: &Option) -> String 52 | where 53 | T: EnumReflectionType, 54 | { 55 | let cases_union = T::generate_cases_union(); 56 | let case_keys_const = T::generate_case_key_const(); 57 | let union_types = T::generate_union_types(&case_key, &content_key, T::inflection()); 58 | let union_type = T::generate_union_schema(); 59 | 60 | // Generate case type 61 | 62 | format!( 63 | r#" 64 | {cases_union} 65 | 66 | {case_keys_const} 67 | {union_types} 68 | {union_type} 69 | "# 70 | ) 71 | } 72 | 73 | trait EnumTypeBridge: EnumReflectionType { 74 | fn case_type_name() -> String { 75 | format!("{}Case", Self::name()) 76 | } 77 | 78 | fn case_key_const_name() -> String { 79 | format!("{}CaseKey", Self::name()) 80 | } 81 | 82 | fn generate_cases_union() -> String { 83 | let mut case_values = vec![]; 84 | let inflection = Self::inflection(); 85 | 86 | for case in Self::cases() { 87 | let inflected = case.name.inflect(inflection); 88 | case_values.push(format!(r#""{inflected}""#)); 89 | } 90 | 91 | let case_values = case_values.join("\n | "); 92 | 93 | let name = Self::case_type_name(); 94 | let cases = case_values; 95 | 96 | ts_string! { 97 | export type #name = #cases; 98 | } 99 | } 100 | 101 | fn generate_case_key_const() -> String { 102 | let mut case_values = String::new(); 103 | let inflection = Self::inflection(); 104 | 105 | case_values.push_str("\n "); 106 | for case in Self::cases() { 107 | let inflected = case.name.inflect(inflection); 108 | case_values.push_str(&format!(r#"{name}: "{inflected}""#, name = case.name,)); 109 | case_values.push_str(",\n "); 110 | } 111 | 112 | let name = Self::case_key_const_name(); 113 | let cases = case_values; 114 | 115 | ts_string! { 116 | export const #name = { 117 | #cases 118 | }; 119 | } 120 | } 121 | 122 | fn generate_union_types( 123 | case_key: &String, 124 | content_key: &Option, 125 | inflection: Inflection, 126 | ) -> String { 127 | let mut result = String::new(); 128 | 129 | for case in Self::cases() { 130 | result.push_str( 131 | Self::generate_union_type(&case, &case_key, &content_key, inflection).as_str(), 132 | ) 133 | } 134 | 135 | result 136 | } 137 | 138 | fn generate_union_type( 139 | case: &EnumCase, 140 | case_key: &String, 141 | content_key: &Option, 142 | inflection: Inflection, 143 | ) -> String { 144 | let case_type_name = union_case_type_name(case, Self::name()); 145 | // let id = Self::case_id(case); 146 | let id = &case.name.inflect(inflection); 147 | 148 | let additional_fields = match &case.type_ { 149 | type_reflect_core::TypeFieldsDefinition::Unit => { 150 | return format!( 151 | r#" 152 | export type {case_type_name} = {{ 153 | {case_key}: "{id}", 154 | }}; 155 | "# 156 | ) 157 | } 158 | type_reflect_core::TypeFieldsDefinition::Tuple(inner) => { 159 | let content_key = match content_key { 160 | Some(content_key) => content_key, 161 | None => { 162 | //TODO: make this a localized Syn error 163 | panic!("Content key required on enums containing at least one tuple-type variant.") 164 | } 165 | }; 166 | if inner.len() == 1 { 167 | let type_ = to_ts_type(&inner[0]); 168 | format!( 169 | r#"{content_key}: {type_}"#, 170 | type_ = type_, 171 | content_key = content_key, 172 | ) 173 | } else { 174 | let tuple_items: Vec = 175 | inner.into_iter().map(|item| to_ts_type(&item)).collect(); 176 | let tuple_items: String = tuple_items.join(",\n "); 177 | 178 | format!( 179 | r#"{content_key}: [ 180 | {tuple_items} 181 | ]"#, 182 | tuple_items = tuple_items, 183 | content_key = content_key, 184 | ) 185 | } 186 | } 187 | type_reflect_core::TypeFieldsDefinition::Named(inner) => { 188 | let struct_items = type_fields::named_fields(inner, case.inflection); 189 | 190 | match content_key { 191 | Some(content_key) => format!( 192 | r#"{content_key}: {{ 193 | {struct_items} 194 | }}"#, 195 | struct_items = struct_items, 196 | content_key = content_key, 197 | ), 198 | None => struct_items, 199 | } 200 | } 201 | }; 202 | format!( 203 | r#" 204 | export type {case_type_name} = {{ 205 | {case_key}: "{id}", 206 | {additional_fields} 207 | }}; 208 | "# 209 | ) 210 | } 211 | 212 | fn generate_union_schema() -> String { 213 | let cases: Vec = Self::cases() 214 | .into_iter() 215 | .map(|case| union_case_type_name(&case, Self::name())) 216 | .collect(); 217 | 218 | let cases = cases.join("\n | "); 219 | 220 | format!( 221 | r#" 222 | export type {name} = {cases}; 223 | "#, 224 | cases = cases, 225 | name = Self::name() 226 | ) 227 | } 228 | } 229 | 230 | pub fn union_case_type_name(case: &EnumCase, parent_name: &str) -> String { 231 | format!("{}Case{}", parent_name, case.name) 232 | } 233 | 234 | impl EnumTypeBridge for T where T: EnumReflectionType {} 235 | -------------------------------------------------------------------------------- /type_reflect/src/type_script/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | pub use super::struct_type::*; 4 | pub use super::type_description::Type; 5 | use super::*; 6 | 7 | pub mod struct_type; 8 | use dprint_plugin_typescript::{ 9 | configuration::{ 10 | ConfigurationBuilder, NextControlFlowPosition, PreferHanging, QuoteStyle, 11 | SameOrNextLinePosition, 12 | }, 13 | FormatTextOptions, 14 | }; 15 | use struct_type::*; 16 | 17 | pub mod enum_type; 18 | pub use enum_type::*; 19 | pub mod untagged_enum_type; 20 | 21 | pub mod type_fields; 22 | pub use type_fields::*; 23 | 24 | mod alias_type; 25 | pub use alias_type::*; 26 | 27 | pub struct TypeScript { 28 | pub tab_size: u32, 29 | } 30 | 31 | impl Default for TypeScript { 32 | fn default() -> Self { 33 | Self { tab_size: 2 } 34 | } 35 | } 36 | 37 | pub trait TypeExporter { 38 | fn export() -> String; 39 | } 40 | 41 | pub fn to_ts_type(t: &Type) -> String { 42 | match t { 43 | // TODO: Support generics 44 | Type::Named(t) => format!("{}", t.name), 45 | Type::String => "string".to_string(), 46 | Type::Int => "number".to_string(), 47 | Type::UnsignedInt => "number".to_string(), 48 | Type::Float => "number".to_string(), 49 | Type::Boolean => "boolean".to_string(), 50 | Type::Option(t) => format!("{}", to_ts_type(t)), 51 | Type::Array(t) => format!("Array<{}>", to_ts_type(t)), 52 | Type::Map { key, value } => { 53 | format!( 54 | "{{[key: {k}]: {v}}}", 55 | k = to_ts_type(key), 56 | v = to_ts_type(value) 57 | ) 58 | } 59 | Type::Transparent(t) => to_ts_type(&*(t.type_)), 60 | } 61 | } 62 | 63 | impl TypeEmitter for TypeScript { 64 | fn prefix(&mut self) -> String { 65 | "".to_string() 66 | } 67 | 68 | fn emit_struct(&mut self) -> String 69 | where 70 | T: StructType, 71 | { 72 | let name = T::name(); 73 | struct_impl(&name, &T::fields(), T::inflection()) 74 | } 75 | 76 | fn emit_enum(&mut self) -> String 77 | where 78 | T: EnumReflectionType, 79 | { 80 | emit_enum_type::() 81 | } 82 | 83 | fn emit_alias(&mut self) -> String 84 | where 85 | T: AliasType, 86 | { 87 | emit_alias_type::() 88 | } 89 | 90 | fn finalize

(&mut self, path: P) -> Result<(), std::io::Error> 91 | where 92 | P: AsRef, 93 | { 94 | // build the configuration once 95 | let config = ConfigurationBuilder::new() 96 | .indent_width(self.tab_size as u8) 97 | .line_width(80) 98 | .build(); 99 | 100 | let file_path = Path::new(&path); 101 | 102 | let text: String = std::fs::read_to_string(Path::new(&path))?; 103 | 104 | let options: FormatTextOptions = FormatTextOptions { 105 | path: Path::new(&path), 106 | extension: None, 107 | text, 108 | config: &config, 109 | external_formatter: None, 110 | }; 111 | 112 | let result = dprint_plugin_typescript::format_text(options); 113 | 114 | match result { 115 | Ok(Some(contents)) => { 116 | std::fs::write(file_path, contents)?; 117 | } 118 | Err(e) => { 119 | eprintln!("Error formatting typescript: {}", e); 120 | } 121 | _ => {} 122 | }; 123 | 124 | Ok(()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /type_reflect/src/type_script/struct_type.rs: -------------------------------------------------------------------------------- 1 | use super::{named_fields, tuple_fields}; 2 | use ts_quote::ts_string; 3 | use type_reflect_core::{Inflection, TypeFieldsDefinition}; 4 | 5 | pub fn struct_impl(name: &str, fields: &TypeFieldsDefinition, inflection: Inflection) -> String { 6 | let fields = match fields { 7 | TypeFieldsDefinition::Unit => todo!(), 8 | TypeFieldsDefinition::Tuple(tuple) => { 9 | let fields = tuple_fields(tuple); 10 | ts_string! { 11 | #fields 12 | } 13 | } 14 | TypeFieldsDefinition::Named(named) => { 15 | let fields = named_fields(named, inflection); 16 | ts_string! { 17 | { #fields } 18 | } 19 | } 20 | }; 21 | ts_string! { 22 | export type #name = #fields; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /type_reflect/src/type_script/type_fields.rs: -------------------------------------------------------------------------------- 1 | use ts_quote::ts_string; 2 | use type_reflect_core::{Inflectable, Inflection, NamedField, Type}; 3 | 4 | use crate::type_script::to_ts_type; 5 | 6 | pub fn named_member(member: &NamedField, inflection: Inflection) -> String { 7 | let name = &member.name.inflect(inflection); 8 | 9 | match &member.type_ { 10 | type_reflect_core::Type::Option(t) => { 11 | let value = to_ts_type(&t); 12 | format!("{name}?: {value};", name = name, value = value) 13 | } 14 | t => { 15 | let value = to_ts_type(&t); 16 | format!("{name}: {value};", name = name, value = value) 17 | } 18 | } 19 | } 20 | 21 | pub fn named_fields(fields: &Vec, inflection: Inflection) -> String { 22 | let members: Vec = fields 23 | .into_iter() 24 | .map(|field| named_member(field, inflection)) 25 | .collect(); 26 | members.join("\n ") 27 | } 28 | 29 | pub fn tuple_fields(fields: &Vec) -> String { 30 | if fields.len() == 1 { 31 | let type_ = to_ts_type(&fields[0]); 32 | ts_string! { #type_ } 33 | } else { 34 | let tuple_items: Vec = fields.into_iter().map(|item| to_ts_type(&item)).collect(); 35 | let tuple_items = tuple_items.join(",\n "); 36 | ts_string! { [ #tuple_items ] } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /type_reflect/src/type_script/untagged_enum_type.rs: -------------------------------------------------------------------------------- 1 | use crate::EnumReflectionType; 2 | use ts_quote::ts_string; 3 | use type_reflect_core::{ 4 | EnumCase, Inflectable, Inflection, NamedField, Type, TypeFieldsDefinition, 5 | }; 6 | 7 | use super::{to_ts_type, type_fields, union_case_type_name}; 8 | 9 | pub fn emit_untaggedd_enum_type() -> String 10 | where 11 | T: EnumReflectionType, 12 | { 13 | let name = T::name(); 14 | let cases = T::cases(); 15 | let inflection = T::inflection(); 16 | 17 | let unit_cases: Vec<&EnumCase> = cases 18 | .iter() 19 | .filter(|c| { 20 | if let TypeFieldsDefinition::Unit = c.type_ { 21 | true 22 | } else { 23 | false 24 | } 25 | }) 26 | .collect(); 27 | 28 | let non_union_cases: Vec<&EnumCase> = cases 29 | .iter() 30 | .filter(|c| { 31 | if let TypeFieldsDefinition::Unit = c.type_ { 32 | false 33 | } else { 34 | true 35 | } 36 | }) 37 | .collect(); 38 | 39 | let unit_cases: Vec = unit_cases 40 | .iter() 41 | .map(|case| emit_unit_case(&case, inflection)) 42 | .collect(); 43 | 44 | let unit_cases: Option = if unit_cases.is_empty() { 45 | None 46 | } else { 47 | Some(unit_cases.join(" | ")) 48 | }; 49 | 50 | let member_cases: Vec = non_union_cases 51 | .iter() 52 | .map(|case| emit_member_case(&case, T::name(), inflection)) 53 | .collect(); 54 | 55 | let member_cases_block = if member_cases.is_empty() { 56 | None 57 | } else { 58 | let cases = member_cases.join(",\n"); 59 | Some(ts_string! { 60 | { 61 | #cases 62 | } 63 | }) 64 | }; 65 | 66 | let member_case_types: Vec = non_union_cases 67 | .iter() 68 | .map(|case| emit_case_type(&case, T::name())) 69 | .collect(); 70 | let member_case_types = member_case_types.join("\n"); 71 | 72 | match (unit_cases, member_cases_block) { 73 | (None, None) => ts_string! { 74 | export type #name = never; 75 | }, 76 | (None, Some(members)) => ts_string! { 77 | #member_case_types 78 | 79 | export type #name = #members; 80 | }, 81 | (Some(units), None) => ts_string! { 82 | export type #name = #units; 83 | }, 84 | (Some(units), Some(members)) => ts_string! { 85 | #member_case_types 86 | 87 | export type #name = #units | #members; 88 | }, 89 | } 90 | } 91 | 92 | fn emit_unit_case(case: &EnumCase, inflection: Inflection) -> String { 93 | let name = &case.name.inflect(inflection); 94 | ts_string! { #"'#name'" } 95 | } 96 | 97 | fn emit_member_case(case: &EnumCase, parent_name: &str, inflection: Inflection) -> String { 98 | let name = &case.name.inflect(inflection); 99 | let member_type = emit_case_type_name(&case, parent_name); 100 | ts_string! { #name ? : #member_type } 101 | } 102 | 103 | pub fn emit_case_type_name(case: &EnumCase, parent_name: &str) -> String { 104 | match &case.type_ { 105 | TypeFieldsDefinition::Unit => unreachable!("unit cases don't have a a case type"), 106 | TypeFieldsDefinition::Tuple(items) => emit_tuple_case_type_name(&case, &items, parent_name), 107 | TypeFieldsDefinition::Named(named_fields) => union_case_type_name(case, parent_name), 108 | } 109 | } 110 | 111 | fn emit_tuple_case_type_name( 112 | case: &EnumCase, 113 | tuple_fields: &Vec, 114 | parent_name: &str, 115 | ) -> String { 116 | if let Some(field) = tuple_fields.first() 117 | && tuple_fields.len() == 1 118 | { 119 | to_ts_type(&field) 120 | } else { 121 | union_case_type_name(case, parent_name) 122 | } 123 | } 124 | 125 | fn emit_case_type(case: &EnumCase, parent_name: &str) -> String { 126 | let name = emit_case_type_name(case, parent_name); 127 | let contents = emit_case_type_contents(case, parent_name); 128 | 129 | if name == contents { 130 | return "".to_string(); 131 | } 132 | 133 | ts_string! { 134 | export type #name = #contents; 135 | } 136 | } 137 | 138 | fn emit_case_type_contents(case: &EnumCase, parent_name: &str) -> String { 139 | match &case.type_ { 140 | TypeFieldsDefinition::Unit => unreachable!("unit cases don't have a a case type"), 141 | TypeFieldsDefinition::Tuple(items) => { 142 | emit_tuple_case_type_contentns(&case, &items, parent_name) 143 | } 144 | TypeFieldsDefinition::Named(named_fields) => { 145 | emit_struct_case_type_contentns(case, named_fields) 146 | } 147 | } 148 | } 149 | 150 | fn emit_tuple_case_type_contentns( 151 | case: &EnumCase, 152 | tuple_fields: &Vec, 153 | parent_name: &str, 154 | ) -> String { 155 | if let Some(field) = tuple_fields.first() 156 | && tuple_fields.len() == 1 157 | { 158 | to_ts_type(&field) 159 | } else { 160 | let members: Vec = tuple_fields 161 | .into_iter() 162 | .map(|field| to_ts_type(&field)) 163 | .collect(); 164 | let members = members.join(", "); 165 | 166 | ts_string! { 167 | [ #members ] 168 | } 169 | } 170 | } 171 | 172 | fn emit_struct_case_type_contentns(case: &EnumCase, named_fields: &Vec) -> String { 173 | let struct_items = type_fields::named_fields(named_fields, case.inflection); 174 | 175 | ts_string! { 176 | { 177 | #struct_items 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /type_reflect/src/zod/alias_type.rs: -------------------------------------------------------------------------------- 1 | use crate::AliasType; 2 | 3 | use super::to_zod_type; 4 | 5 | pub fn emit_alias_type() -> String 6 | where 7 | T: AliasType, 8 | { 9 | return format!( 10 | r#" 11 | 12 | export const {name}Schema = {schema}; 13 | export type {name} = z.infer; 14 | 15 | "#, 16 | name = T::name(), 17 | schema = to_zod_type(&T::source_type()) 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /type_reflect/src/zod/enum_type.rs: -------------------------------------------------------------------------------- 1 | use type_reflect_core::{EnumCase, EnumType, Inflectable, Inflection}; 2 | 3 | use crate::EnumReflectionType; 4 | 5 | use super::to_zod_type; 6 | 7 | pub fn emit_enum_type() -> String 8 | where 9 | T: EnumReflectionType, 10 | { 11 | match T::enum_type() { 12 | EnumType::Simple => emit_simple_enum_type::(), 13 | EnumType::Complex { 14 | case_key, 15 | content_key, 16 | } => emit_complex_enum_type::(&case_key, &content_key), 17 | EnumType::Untagged => unimplemented!("untagged enums not supported by the Zod generator"), 18 | } 19 | } 20 | 21 | fn emit_simple_enum_type() -> String 22 | where 23 | T: EnumReflectionType, 24 | { 25 | let simple_cases: String = T::cases() 26 | .into_iter() 27 | .map(|case| { 28 | format!( 29 | r#" {name} = "{name}", 30 | "#, 31 | name = case.name 32 | ) 33 | }) 34 | .collect(); 35 | 36 | let schema_name = T::union_schema_name(); 37 | let schema_cases: String = T::cases() 38 | .into_iter() 39 | .map(|case| { 40 | format!( 41 | " {enum_name}.{case_name},\n", 42 | enum_name = T::name(), 43 | case_name = case.name 44 | ) 45 | }) 46 | .collect(); 47 | 48 | format!( 49 | r#" 50 | export enum {name} {{ 51 | {simple_cases}}} 52 | 53 | export const {schema_name} = z.enum([ 54 | {schema_cases}]) 55 | "#, 56 | name = T::name(), 57 | simple_cases = simple_cases, 58 | schema_name = schema_name, 59 | schema_cases = schema_cases 60 | ) 61 | } 62 | 63 | fn emit_complex_enum_type(case_key: &String, content_key: &Option) -> String 64 | where 65 | T: EnumReflectionType, 66 | { 67 | let cases_enum = T::generate_cases_enum(); 68 | let union_types = T::generate_union_types(&case_key, &content_key, T::inflection()); 69 | let union_type = T::generate_union_schema(); 70 | 71 | // Generate case type 72 | 73 | // let members = enum_cases(&T::cases()); 74 | 75 | format!( 76 | r#" 77 | {cases_enum} 78 | {union_types} 79 | {union_type} 80 | "#, 81 | cases_enum = cases_enum, 82 | union_types = union_types, 83 | union_type = union_type 84 | ) 85 | } 86 | 87 | trait EnumTypeBridge: EnumReflectionType { 88 | fn case_type_name() -> String { 89 | format!("{}Case", Self::name()) 90 | } 91 | 92 | fn case_id(case: &EnumCase) -> String { 93 | format!("{}.{}", Self::case_type_name(), case.name) 94 | } 95 | 96 | fn generate_cases_enum() -> String { 97 | let mut case_values = String::new(); 98 | for case in Self::cases() { 99 | case_values.push_str(format!(r#" {name} = "{name}""#, name = case.name).as_str()); 100 | case_values.push_str(",\n"); 101 | } 102 | 103 | format!( 104 | r#" 105 | export enum {name} {{ 106 | {cases}}} 107 | "#, 108 | name = Self::case_type_name(), 109 | cases = case_values 110 | ) 111 | } 112 | 113 | fn generate_union_types( 114 | case_key: &String, 115 | content_key: &Option, 116 | inflection: Inflection, 117 | ) -> String { 118 | let mut result = String::new(); 119 | 120 | for case in Self::cases() { 121 | result.push_str( 122 | Self::generate_union_type(&case, &case_key, &content_key, inflection).as_str(), 123 | ) 124 | } 125 | 126 | result 127 | } 128 | 129 | fn generate_union_type( 130 | case: &EnumCase, 131 | case_key: &String, 132 | content_key: &Option, 133 | _inflection: Inflection, 134 | ) -> String { 135 | let schema_name = union_type_name(case, Self::name()); 136 | let id = Self::case_id(case); 137 | 138 | let additional_fields = match &case.type_ { 139 | type_reflect_core::TypeFieldsDefinition::Unit => String::new(), 140 | type_reflect_core::TypeFieldsDefinition::Tuple(inner) => { 141 | let content_key = match content_key { 142 | Some(content_key) => content_key, 143 | None => { 144 | //TODO: make this a localized Syn error 145 | panic!("Content key required on enums containing at least one tuple-type variant.") 146 | } 147 | }; 148 | if inner.len() == 1 { 149 | let type_ = to_zod_type(&inner[0]); 150 | format!( 151 | r#" {content_key}: {type_}"#, 152 | type_ = type_, 153 | content_key = content_key, 154 | ) 155 | } else { 156 | let tuple_items: String = inner 157 | .into_iter() 158 | .map(|item| format!(" {},\n", to_zod_type(&item))) 159 | .collect(); 160 | 161 | format!( 162 | r#" {content_key}: z.tuple([ 163 | {tuple_items} ])"#, 164 | tuple_items = tuple_items, 165 | content_key = content_key, 166 | ) 167 | } 168 | } 169 | type_reflect_core::TypeFieldsDefinition::Named(inner) => { 170 | let struct_items: String = inner 171 | .into_iter() 172 | .map(|item| { 173 | format!( 174 | " {}: {},\n", 175 | item.name.inflect(case.inflection), 176 | to_zod_type(&item.type_) 177 | ) 178 | }) 179 | .collect(); 180 | 181 | match content_key { 182 | Some(content_key) => format!( 183 | r#" {content_key}: z.object({{ 184 | {struct_items} }})"#, 185 | struct_items = struct_items, 186 | content_key = content_key, 187 | ), 188 | None => struct_items, 189 | } 190 | } 191 | }; 192 | format!( 193 | r#" 194 | export const {schema_name} = z.object({{ 195 | {case_key}: z.literal({id}), 196 | {additional_fields}}}); 197 | export type {name} = z.infer 198 | "#, 199 | schema_name = schema_name, 200 | name = format!("{}Case{}", Self::name(), case.name), 201 | case_key = case_key, 202 | id = id, 203 | additional_fields = additional_fields 204 | ) 205 | } 206 | 207 | fn union_schema_name() -> String { 208 | format!("{}Schema", Self::name()) 209 | } 210 | 211 | fn generate_union_schema() -> String { 212 | let schema_name = Self::union_schema_name(); 213 | let mut cases = String::new(); 214 | 215 | for case in Self::cases() { 216 | cases.push_str(format!(" {},\n", union_type_name(&case, Self::name())).as_str()); 217 | } 218 | 219 | format!( 220 | r#" 221 | export const {schema_name} = z.union([ 222 | {cases}]); 223 | export type {name} = z.infer 224 | "#, 225 | cases = cases, 226 | schema_name = schema_name, 227 | name = Self::name() 228 | ) 229 | } 230 | } 231 | 232 | fn union_type_name(case: &EnumCase, parent_name: &str) -> String { 233 | format!("{}Case{}Schema", parent_name, case.name) 234 | } 235 | 236 | impl EnumTypeBridge for T where T: EnumReflectionType {} 237 | -------------------------------------------------------------------------------- /type_reflect/src/zod/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | pub use super::struct_type::*; 4 | pub use super::type_description::Type; 5 | use super::*; 6 | 7 | mod struct_type; 8 | use struct_type::*; 9 | 10 | mod enum_type; 11 | use enum_type::*; 12 | 13 | mod alias_type; 14 | use alias_type::*; 15 | 16 | #[derive(Default)] 17 | pub struct Zod {} 18 | 19 | pub trait TypeExporter { 20 | fn export() -> String; 21 | } 22 | 23 | fn to_zod_type(t: &Type) -> String { 24 | match t { 25 | // TODO: support generics 26 | Type::Named(t) => format!("{}Schema", t.name), 27 | Type::String => "z.string()".to_string(), 28 | Type::Int => "z.number()".to_string(), 29 | Type::UnsignedInt => "z.number()".to_string(), 30 | Type::Float => "z.number()".to_string(), 31 | Type::Boolean => "z.bool()".to_string(), 32 | Type::Option(t) => format!("{}.optional()", to_zod_type(t)), 33 | Type::Array(t) => format!("z.array({})", to_zod_type(t)), 34 | Type::Map { key, value } => format!("z.map({}, {})", to_zod_type(key), to_zod_type(value)), 35 | Type::Transparent(_t) => unimplemented!("Transparent types not yet implemented for Zod"), 36 | } 37 | } 38 | 39 | impl TypeEmitter for Zod { 40 | fn prefix(&mut self) -> String { 41 | "import { z } from 'zod';\n".to_string() 42 | } 43 | 44 | fn emit_struct(&mut self) -> String 45 | where 46 | T: StructType, 47 | { 48 | let members = struct_fields(&T::fields(), T::inflection()); 49 | let name = T::name(); 50 | 51 | format!( 52 | r#" 53 | 54 | export const {name}Schema = z.object({{ 55 | {members}}}); 56 | 57 | export type {name} = z.infer; 58 | 59 | "#, 60 | members = members, 61 | name = name 62 | ) 63 | } 64 | 65 | fn emit_enum(&mut self) -> String 66 | where 67 | T: EnumReflectionType, 68 | { 69 | emit_enum_type::() 70 | } 71 | 72 | fn emit_alias(&mut self) -> String 73 | where 74 | T: AliasType, 75 | { 76 | emit_alias_type::() 77 | } 78 | 79 | fn finalize

(&mut self, _path: P) -> Result<(), std::io::Error> 80 | where 81 | P: AsRef, 82 | { 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /type_reflect/src/zod/struct_type.rs: -------------------------------------------------------------------------------- 1 | use crate::zod::to_zod_type; 2 | use ts_quote::*; 3 | use type_reflect_core::{Inflectable, Inflection, NamedField, TypeFieldsDefinition}; 4 | 5 | pub fn struct_member(member: &NamedField, inflection: Inflection) -> String { 6 | let name = &member.name.inflect(inflection); 7 | let value = to_zod_type(&member.type_); 8 | ts_string! { #name: #value, } 9 | 10 | // format!(" {name}: {value},\n", name = name, value = value) 11 | } 12 | 13 | pub fn named_fields(fields: &Vec, inflection: Inflection) -> String { 14 | let mut result = String::new(); 15 | for member in fields { 16 | result.push_str(struct_member(member, inflection).as_str()) 17 | } 18 | result 19 | } 20 | 21 | pub fn struct_fields(fields: &TypeFieldsDefinition, inflection: Inflection) -> String { 22 | match fields { 23 | TypeFieldsDefinition::Unit => todo!(), 24 | TypeFieldsDefinition::Tuple(_) => todo!(), 25 | TypeFieldsDefinition::Named(named) => named_fields(named, inflection), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /type_reflect/tests/.gitignore: -------------------------------------------------------------------------------- 1 | output/src/* 2 | output/node_modules/ 3 | -------------------------------------------------------------------------------- /type_reflect/tests/common.rs: -------------------------------------------------------------------------------- 1 | pub const OUTPUT_DIR: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output"); 2 | 3 | #[allow(unused)] 4 | pub const TESTING_PREFIX: &'static str = r#" 5 | 6 | function assertThrows(fn: ()=>void, message: string) { 7 | try { 8 | fn(); 9 | } catch (e) { 10 | console.log(`error thrown: ${e}`); 11 | return; 12 | } 13 | throw new Error(message); 14 | } 15 | 16 | function assertDoesNotThrow(fn: ()=>T, message: string) { 17 | try { 18 | return fn(); 19 | } catch (e) { 20 | console.error(message); 21 | throw e; 22 | } 23 | } 24 | 25 | "#; 26 | 27 | use std::{fs, path::PathBuf}; 28 | 29 | use anyhow::{bail, Result}; 30 | use std::process::Command; 31 | 32 | #[allow(unused)] 33 | pub struct OutputLocation { 34 | path: PathBuf, 35 | filename: String, 36 | } 37 | 38 | #[allow(unused)] 39 | fn run_command(dir: &str, command: &str) -> Result<()> { 40 | println!("Running command:\n\t{}", command); 41 | 42 | let mut parts = command.split_whitespace(); 43 | let command = parts.next().expect("no command given"); 44 | let args = parts.collect::>(); 45 | 46 | let mut child = Command::new(command).args(&args).current_dir(dir).spawn()?; // Spawn the command as a child process 47 | 48 | let status = child.wait()?; // Wait for the command to complete 49 | 50 | if status.success() { 51 | } else { 52 | bail!("Command failed: {}", command) 53 | } 54 | Ok(()) 55 | } 56 | 57 | #[allow(unused)] 58 | impl OutputLocation { 59 | pub fn ts_path(&self) -> PathBuf { 60 | self.path.with_extension("ts") 61 | } 62 | fn jest_path(&self) -> PathBuf { 63 | self.path.with_extension("test.ts") 64 | } 65 | fn clean(&self) { 66 | remove_file(self.ts_path()); 67 | remove_file(self.jest_path()); 68 | } 69 | 70 | pub fn run_ts(&self) -> Result<()> { 71 | println!(""); 72 | run_command( 73 | OUTPUT_DIR, 74 | format!("yarn jest {}", self.jest_path().to_str().unwrap()).as_str(), 75 | )?; 76 | Ok(()) 77 | } 78 | 79 | pub fn write_jest(&self, imports: &str, content: &str) -> Result<()> { 80 | fs::write( 81 | self.jest_path(), 82 | format!( 83 | "import {{ {imports} }} from './{file}'\n\n{content}", 84 | content = content, 85 | imports = imports, 86 | file = self.filename 87 | ), 88 | )?; 89 | Ok(()) 90 | } 91 | } 92 | 93 | fn remove_file(path: PathBuf) { 94 | match fs::remove_file(&path) { 95 | Ok(_) => {} 96 | Err(e) => eprintln!("Error removing file: [{:?}]: {}", &path, e), 97 | } 98 | } 99 | 100 | pub fn init_path(scope: &str, name: &str) -> OutputLocation { 101 | let mut base_path: PathBuf = PathBuf::from(OUTPUT_DIR); 102 | base_path.push("src"); 103 | base_path.push(scope); 104 | if !base_path.exists() { 105 | match fs::create_dir(&base_path) { 106 | Ok(_) => {} 107 | Err(e) => { 108 | eprintln!("Error creating directory [{:?}]: {}", &base_path, e); 109 | } 110 | } 111 | }; 112 | 113 | base_path.push(name); 114 | let output = OutputLocation { 115 | path: base_path, 116 | filename: name.to_string(), 117 | }; 118 | output.clean(); 119 | output 120 | } 121 | -------------------------------------------------------------------------------- /type_reflect/tests/output/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | // If you have a src folder where your TypeScript files reside 5 | roots: ['/src/'], 6 | }; 7 | -------------------------------------------------------------------------------- /type_reflect/tests/output/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "output", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@types/jest": "^29.5.8", 8 | "jest": "^29.7.0", 9 | "ts-jest": "^29.1.1", 10 | "typescript": "^5.2.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /type_reflect/tests/output/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "strict": true, 9 | "esModuleInterop": true, 10 | }, 11 | "include": [ 12 | "src/**/*" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | "dist" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /type_reflect/tests/test_adt.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use type_reflect::*; 8 | 9 | #[derive(Reflect, Serialize, Deserialize)] 10 | pub struct Rectangle { 11 | width: f32, 12 | height: f32, 13 | } 14 | 15 | #[derive(Reflect, Serialize, Deserialize)] 16 | #[serde(tag = "_case", content = "data")] 17 | pub enum Shape { 18 | Circle { radius: f32 }, 19 | Square { side: f32 }, 20 | Rectangle(Rectangle), 21 | ScaledRectangle(Rectangle, u32), 22 | Null, 23 | } 24 | 25 | pub const SCOPE: &'static str = "test_adt"; 26 | 27 | #[test] 28 | fn test_validation() -> Result<()> { 29 | let output = init_path(SCOPE, "test_validation"); 30 | 31 | export_types!( 32 | types: [ Shape, Rectangle ], 33 | destinations: [( 34 | output.ts_path(), 35 | emitters: [ 36 | TypeScript(), 37 | TSValidation(), 38 | TSFormat( 39 | tab_size: 2, 40 | line_width: 80, 41 | ), 42 | ], 43 | )] 44 | )?; 45 | 46 | output.write_jest( 47 | "Shape, Rectangle, ShapeCase, ShapeCaseKey", 48 | ts_string! { 49 | describe("ADT Validation", ()=>{ 50 | it("Validates a Null variant: ShapeCaseKey.Null", ()=>{ 51 | expect(() => { 52 | Shape.validate({_case: ShapeCaseKey.Null}) 53 | }).not.toThrow(); 54 | }); 55 | it("Validates a Null variant literal: 'Null'", ()=>{ 56 | expect(() => { 57 | Shape.validate({_case: "Null"}) 58 | }).not.toThrow(); 59 | }); 60 | it("Validates a Circle variant: {_case: ShapeCaseKey.Circle, data: { radius: 1.7} }", ()=>{ 61 | expect(() => { 62 | Shape.validate({ 63 | _case: ShapeCaseKey.Circle, 64 | data: { 65 | radius: 1.7 66 | } 67 | }) 68 | }).not.toThrow(); 69 | }); 70 | it("Validates a Rectangle variant: {_case: ShapeCaseKey.Rectangle, data: { width: 1, height: 2} }", ()=>{ 71 | expect(() => { 72 | Shape.validate({ 73 | _case: ShapeCaseKey.Rectangle, 74 | data: { 75 | width: 1, 76 | height: 2 77 | } 78 | }) 79 | }).not.toThrow(); 80 | }); 81 | it("Validates a ScaledRectangle variant: {_case: ShapeCaseKey.ScaledRectangle, data: [{ width: 1, height: 2}, 0.5] }", ()=>{ 82 | expect(() => { 83 | Shape.validate({ 84 | _case: ShapeCaseKey.ScaledRectangle, 85 | data: [ 86 | { 87 | width: 1, 88 | height: 2 89 | }, 90 | 0.5 91 | ] 92 | }) 93 | }).not.toThrow(); 94 | }); 95 | it("Doesn't Validate an incorrect ScaledRectangle variant: {_case: ShapeCaseKey.Circle, data: [{ width: 1, height: 2}, 0.5] }", ()=>{ 96 | expect(() => { 97 | Shape.validate({ 98 | _case: ShapeCaseKey.Circle, 99 | data: [ 100 | { 101 | width: 1, 102 | height: 2 103 | }, 104 | 0.5 105 | ] 106 | }) 107 | }).toThrow(); 108 | }); 109 | 110 | }); 111 | } 112 | .as_str(), 113 | )?; 114 | 115 | output.run_ts() 116 | } 117 | -------------------------------------------------------------------------------- /type_reflect/tests/test_array.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /type_reflect/tests/test_boxed.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use ts_quote::*; 8 | use type_reflect::*; 9 | 10 | pub const SCOPE: &'static str = "test_boxed"; 11 | 12 | #[derive(Reflect, Serialize, Deserialize)] 13 | pub struct Bar { 14 | val: bool, 15 | } 16 | 17 | #[derive(Reflect, Serialize, Deserialize)] 18 | pub struct BoxOfPrimitive { 19 | boxed: Box, 20 | // boxed: u32, 21 | } 22 | 23 | #[derive(Reflect, Serialize, Deserialize)] 24 | pub struct BoxOfType { 25 | boxed: Box, 26 | } 27 | 28 | #[test] 29 | fn test_box_of_primitive() -> Result<()> { 30 | let output = init_path(SCOPE, "test_box_of_primitive"); 31 | 32 | export_types!( 33 | types: [ BoxOfPrimitive ], 34 | destinations: [( 35 | output.ts_path(), 36 | emitters: [ 37 | TypeScript(), 38 | TSValidation(), 39 | TSFormat( 40 | tab_size: 2, 41 | line_width: 80, 42 | ), 43 | ], 44 | )] 45 | )?; 46 | 47 | output.write_jest( 48 | "BoxOfPrimitive", 49 | r#" 50 | 51 | describe('Box of Primitive Validation', ()=>{ 52 | 53 | it('validates an object: `{ boxed: 42 }` which conforms to BoxOfPrimitive', ()=>{ 54 | expect(() => { 55 | BoxOfPrimitive.validate({ boxed: 42 }); 56 | }).not.toThrow(); 57 | }); 58 | 59 | it('throws an error validating a malformed object: `{ boxed: [42] }`', ()=>{ 60 | expect(() => { 61 | BoxOfPrimitive.validate({ boxed: [42] }) 62 | }).toThrow(); 63 | }); 64 | 65 | }) 66 | "#, 67 | )?; 68 | 69 | output.run_ts() 70 | } 71 | 72 | #[test] 73 | fn test_box_of_type() -> Result<()> { 74 | let output = init_path(SCOPE, "test_box_of_type"); 75 | 76 | export_types!( 77 | types: [ Bar, BoxOfType ], 78 | destinations: [( 79 | output.ts_path(), 80 | emitters: [ 81 | TypeScript(), 82 | TSValidation(), 83 | TSFormat( 84 | tab_size: 2, 85 | line_width: 80, 86 | ), 87 | ], 88 | )] 89 | )?; 90 | 91 | output.write_jest( 92 | "Bar, BoxOfType", 93 | r#" 94 | 95 | describe('Box of Type Validation', ()=>{ 96 | 97 | it('validates an object: `{ boxed: { val: true } }` which conforms to BoxOfType', ()=>{ 98 | expect(() => { 99 | BoxOfType.validate({ boxed: { val: true } }); 100 | }).not.toThrow(); 101 | }); 102 | 103 | it('throws an error validating a malformed object: `{ boxed: [true] }`', ()=>{ 104 | expect(() => { 105 | BoxOfType.validate({ boxed: [true] }) 106 | }).toThrow(); 107 | }); 108 | 109 | }) 110 | "#, 111 | )?; 112 | 113 | output.run_ts() 114 | } 115 | 116 | #[derive(Reflect, Serialize, Deserialize)] 117 | #[serde(tag = "_case", content = "data")] 118 | pub enum Tree { 119 | Leaf(bool), 120 | Subtree { left: Box, right: Box }, 121 | } 122 | 123 | #[test] 124 | fn test_nested_box() -> Result<()> { 125 | let output = init_path(SCOPE, "test_nested_box"); 126 | 127 | export_types!( 128 | types: [ Tree ], 129 | destinations: [( 130 | output.ts_path(), 131 | emitters: [ 132 | TypeScript(), 133 | TSValidation(), 134 | TSFormat( 135 | tab_size: 2, 136 | line_width: 80, 137 | ), 138 | ], 139 | )] 140 | )?; 141 | 142 | output.write_jest( 143 | "Tree, TreeCase, TreeCaseKey", 144 | ts_quote! { 145 | 146 | describe("Nested Box Validation", ()=>{ 147 | 148 | it("validates an object: `{ ... }` which conforms to Tree", ()=>{ 149 | expect(() => { 150 | Tree.validate( 151 | { 152 | _case: TreeCaseKey.Subtree, 153 | data: { 154 | left: { _case: TreeCaseKey.Leaf, data: true }, 155 | right: { 156 | _case: TreeCaseKey.Subtree, 157 | data: { 158 | left: { _case: TreeCaseKey.Leaf, data: true }, 159 | right: { _case: TreeCaseKey.Leaf, data: false } 160 | } 161 | } 162 | } 163 | } 164 | ); 165 | }).not.toThrow(); 166 | }); 167 | 168 | it("throws an error validating a malformed object: `{ boxed: [true] }`", ()=>{ 169 | expect(() => { 170 | Tree.validate({ boxed: [true] }) 171 | }).toThrow(); 172 | }); 173 | 174 | }) 175 | 176 | 177 | }? 178 | .formatted(None)? 179 | .as_str(), 180 | )?; 181 | 182 | output.run_ts() 183 | } 184 | -------------------------------------------------------------------------------- /type_reflect/tests/test_map.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use std::collections::{BTreeMap, HashMap}; 4 | 5 | use anyhow::Result; 6 | use common::*; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | use type_reflect::*; 10 | 11 | pub const SCOPE: &'static str = "test_map"; 12 | 13 | #[derive(Reflect, Serialize, Deserialize)] 14 | pub struct Bar { 15 | val: bool, 16 | } 17 | 18 | #[derive(Reflect, Serialize, Deserialize)] 19 | pub struct MapOfPrimitive { 20 | records: HashMap, 21 | } 22 | 23 | #[derive(Reflect, Serialize, Deserialize)] 24 | pub struct MapOfType { 25 | records: HashMap, 26 | } 27 | 28 | #[derive(Reflect, Serialize, Deserialize)] 29 | pub struct BTreeBasedMap { 30 | records: BTreeMap, 31 | } 32 | 33 | #[test] 34 | fn test_map_of_primitive() -> Result<()> { 35 | let output = init_path(SCOPE, "test_map_of_primitive"); 36 | 37 | export_types!( 38 | types: [ MapOfPrimitive ], 39 | destinations: [( 40 | output.ts_path(), 41 | emitters: [ 42 | TypeScript(), 43 | TSValidation(), 44 | TSFormat( 45 | tab_size: 2, 46 | line_width: 80, 47 | ), 48 | ], 49 | )] 50 | )?; 51 | 52 | output.write_jest( 53 | "MapOfPrimitive", 54 | r#" 55 | 56 | describe('Struct with Array of Primitives Validation', ()=>{ 57 | 58 | it('validates an object: `{ records: {a: 42, b: 7, dog: 3, cat: 21} }` which conforms to MapOfPrimitive', ()=>{ 59 | expect(() => { 60 | MapOfPrimitive.validate({ records: {a: 42, b: 7, dog: 3, cat: 21} }); 61 | }).not.toThrow(); 62 | }); 63 | 64 | it('validates an empty array: `{ records: {} }` which conforms to MapOfPrimitive', ()=>{ 65 | expect(() => { 66 | MapOfPrimitive.validate({ records: {} }); 67 | }).not.toThrow(); 68 | }); 69 | 70 | it('throws an error validating an object: `{a: 42, b: 7, dog: "3", cat: 21}` which has one value not conforming to the type', ()=>{ 71 | expect(() => { 72 | MapOfPrimitive.validate({a: 42, b: 7, dog: "3", cat: 21}) 73 | }).toThrow(); 74 | }); 75 | 76 | }) 77 | "#, 78 | )?; 79 | 80 | output.run_ts() 81 | } 82 | 83 | #[test] 84 | fn test_nested_map() -> Result<()> { 85 | let output = init_path(SCOPE, "test_nested_map"); 86 | 87 | export_types!( 88 | types: [ MapOfType, Bar ], 89 | destinations: [( 90 | output.ts_path(), 91 | emitters: [ 92 | TypeScript(), 93 | TSValidation(), 94 | TSFormat( 95 | tab_size: 2, 96 | line_width: 80, 97 | ), 98 | ], 99 | )] 100 | )?; 101 | 102 | output.write_jest( 103 | "MapOfType, Bar", 104 | r#" 105 | 106 | describe('Struct with Map of Types Validation', ()=>{ 107 | 108 | it('validates an object: `{ records: {a: { val: true }, b: {val: false } } }` which conforms to MapOfType', ()=>{ 109 | expect(() => { 110 | MapOfType.validate({ records: {a: { val: true }, b: {val: false } } }); 111 | }).not.toThrow(); 112 | }); 113 | 114 | it('validates an empty object: `{ records: {} }` which conforms to MapOfType', ()=>{ 115 | expect(() => { 116 | MapOfType.validate({ records: {} }); 117 | }).not.toThrow(); 118 | }); 119 | 120 | it('throws an error validating an object: `{ records: {a: { val: true }, b: {val: false }, c: 32 } }` which has one value not conforming to the type', ()=>{ 121 | expect(() => { 122 | MapOfType.validate({ records: {a: { val: true }, b: {val: false }, c: 32 } }) 123 | }).toThrow(); 124 | }); 125 | 126 | }) 127 | "#, 128 | )?; 129 | 130 | output.run_ts() 131 | } 132 | 133 | #[test] 134 | fn test_btree_baseed_map() -> Result<()> { 135 | let output = init_path(SCOPE, "test_btree_baseed_map"); 136 | 137 | export_types!( 138 | types: [ BTreeBasedMap ], 139 | destinations: [( 140 | output.ts_path(), 141 | emitters: [ 142 | TypeScript(), 143 | TSValidation(), 144 | TSFormat( 145 | tab_size: 2, 146 | line_width: 80, 147 | ), 148 | ], 149 | )] 150 | )?; 151 | 152 | output.write_jest( 153 | "BTreeBasedMap", 154 | r#" 155 | 156 | describe('Struct with Array of Primitives Validation', ()=>{ 157 | 158 | it('validates an object: `{ records: {a: 42, b: 7, dog: 3, cat: 21} }` which conforms to BTreeBasedMap', ()=>{ 159 | expect(() => { 160 | BTreeBasedMap.validate({ records: {a: 42, b: 7, dog: 3, cat: 21} }); 161 | }).not.toThrow(); 162 | }); 163 | 164 | it('validates an empty array: `{ records: {} }` which conforms to BTreeBasedMap', ()=>{ 165 | expect(() => { 166 | BTreeBasedMap.validate({ records: {} }); 167 | }).not.toThrow(); 168 | }); 169 | 170 | it('throws an error validating an object: `{a: 42, b: 7, dog: "3", cat: 21}` which has one value not conforming to the type', ()=>{ 171 | expect(() => { 172 | BTreeBasedMap.validate({a: 42, b: 7, dog: "3", cat: 21}) 173 | }).toThrow(); 174 | }); 175 | 176 | }) 177 | "#, 178 | )?; 179 | 180 | output.run_ts() 181 | } 182 | -------------------------------------------------------------------------------- /type_reflect/tests/test_nested.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use type_reflect::*; 8 | 9 | pub const SCOPE: &'static str = "test_nested"; 10 | 11 | #[derive(Reflect, Serialize, Deserialize)] 12 | pub struct Bar { 13 | val: bool, 14 | } 15 | 16 | #[derive(Reflect, Serialize, Deserialize)] 17 | pub struct Foo { 18 | bar: Bar, 19 | } 20 | 21 | #[test] 22 | fn test_validation() -> Result<()> { 23 | let output = init_path(SCOPE, "test_validation"); 24 | 25 | export_types!( 26 | types: [ Foo, Bar ], 27 | destinations: [( 28 | output.ts_path(), 29 | emitters: [ 30 | TypeScript(), 31 | TSValidation(), 32 | TSFormat( 33 | tab_size: 2, 34 | line_width: 80, 35 | ), 36 | ], 37 | )] 38 | )?; 39 | 40 | output.write_jest( 41 | "Foo, Bar", 42 | r#" 43 | 44 | describe('Struct with Nested Type Validation', ()=>{ 45 | 46 | it('validates an object: `{ bar: { val: true } }` which conforms to the nested types', ()=>{ 47 | expect(() => { 48 | Foo.validate({ bar: { val: true } }); 49 | }).not.toThrow(); 50 | }); 51 | 52 | it('throws an error validating an object: `{ bar: { val: "hola" } }` not conforming to the nested type', ()=>{ 53 | expect(() => { 54 | Foo.validate({ bar: { val: "hola" } }) 55 | }).toThrow(); 56 | }); 57 | 58 | it('throws an error validating an object: `{ bar: true }` not conforming to the nested type', ()=>{ 59 | expect(() => { 60 | Foo.validate({ bar: true }) 61 | }).toThrow(); 62 | }); 63 | 64 | it('throws an error validating an object: `{ baz: { val: true } }` not conforming to the outer type', ()=>{ 65 | expect(() => { 66 | Foo.validate({ baz: { val: true } }) 67 | }).toThrow(); 68 | }); 69 | 70 | }) 71 | "#, 72 | )?; 73 | 74 | output.run_ts() 75 | } 76 | -------------------------------------------------------------------------------- /type_reflect/tests/test_optional.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use type_reflect::*; 8 | 9 | #[derive(Reflect, Serialize, Deserialize)] 10 | pub struct Foo { 11 | pub x: f32, 12 | pub y: Option, 13 | } 14 | 15 | pub const SCOPE: &'static str = "test_optional"; 16 | 17 | #[test] 18 | fn test_validation() -> Result<()> { 19 | let output = init_path(SCOPE, "test_validation"); 20 | 21 | export_types!( 22 | types: [ Foo ], 23 | destinations: [( 24 | output.ts_path(), 25 | emitters: [ 26 | TypeScript(), 27 | TSValidation(), 28 | TSFormat( 29 | tab_size: 2, 30 | line_width: 80, 31 | ), 32 | ], 33 | )] 34 | )?; 35 | 36 | output.write_jest( 37 | "Foo", 38 | r#" 39 | 40 | describe('Struct with Optional Member Validation', ()=>{ 41 | 42 | it("validates an object: `{x: 7, y: 42}` with both the requred and optional members", ()=>{ 43 | expect(() => { 44 | Foo.validate({x: 7, y: 42}) 45 | }).not.toThrow(); 46 | }); 47 | 48 | it("validates an object: `{x: 7}` matching the type `Foo` without the optional member `y`", ()=>{ 49 | expect(() => { 50 | Foo.validate({x: 7}) 51 | }).not.toThrow(); 52 | }); 53 | 54 | it("throws an error validating an object: `{y: 42}` missing the required member `x`", ()=>{ 55 | expect(() => { 56 | Foo.validate({y: 42}) 57 | }).toThrow(); 58 | }); 59 | 60 | }) 61 | "#, 62 | )?; 63 | 64 | output.run_ts() 65 | } 66 | -------------------------------------------------------------------------------- /type_reflect/tests/test_simple_enum.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use type_reflect::*; 8 | 9 | #[derive(Reflect, Serialize, Deserialize)] 10 | pub enum Pet { 11 | Dog, 12 | Cat, 13 | } 14 | 15 | pub const SCOPE: &'static str = "test_simple_enum"; 16 | 17 | #[test] 18 | fn test_validation() -> Result<()> { 19 | let output = init_path(SCOPE, "test_validation"); 20 | 21 | export_types!( 22 | types: [ Pet ], 23 | destinations: [( 24 | output.ts_path(), 25 | emitters: [ 26 | TypeScript(), 27 | TSValidation(), 28 | TSFormat( 29 | tab_size: 2, 30 | line_width: 80, 31 | ), 32 | ], 33 | )] 34 | )?; 35 | 36 | output.write_jest( 37 | "Pet", 38 | r#" 39 | 40 | describe('Simple Enum Validation', ()=>{ 41 | 42 | it("validates an object: `Dog`", ()=>{ 43 | expect(() => { 44 | Pet.validate(`Dog`) 45 | }).not.toThrow(); 46 | }); 47 | 48 | it("validates an object: `Cat`", ()=>{ 49 | expect(() => { 50 | Pet.validate(`Cat`) 51 | }).not.toThrow(); 52 | }); 53 | 54 | it("throws an error validating an number: `7`", ()=>{ 55 | expect(() => { 56 | Pet.validate(7) 57 | }).toThrow(); 58 | }); 59 | 60 | it("throws an error validating an object: `{tag: 'Dog'}`", ()=>{ 61 | expect(() => { 62 | Pet.validate({tag: 'Dog'}) 63 | }).toThrow(); 64 | }); 65 | 66 | }) 67 | "#, 68 | )?; 69 | 70 | output.run_ts() 71 | } 72 | -------------------------------------------------------------------------------- /type_reflect/tests/test_simple_struct.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use type_reflect::*; 8 | 9 | #[derive(Reflect, Serialize, Deserialize)] 10 | pub struct Foo { 11 | pub x: f32, 12 | } 13 | 14 | pub const SCOPE: &'static str = "test_simple_struct"; 15 | 16 | #[test] 17 | fn test_validation() -> Result<()> { 18 | let output = init_path(SCOPE, "test_validation"); 19 | 20 | export_types!( 21 | types: [ Foo ], 22 | destinations: [( 23 | output.ts_path(), 24 | emitters: [ 25 | TypeScript(), 26 | TSValidation(), 27 | TSFormat( 28 | tab_size: 2, 29 | line_width: 80, 30 | ), 31 | ], 32 | )] 33 | )?; 34 | 35 | output.write_jest( 36 | "Foo", 37 | r#" 38 | 39 | describe('Simple Struct Validation', ()=>{ 40 | it("validates an object: `{x: 7}` matching the type `Foo` without throwing", ()=>{ 41 | expect(() => { 42 | Foo.validate({x: 7}) 43 | }).not.toThrow(); 44 | }); 45 | 46 | it("validates an object: `{x: -7}` matching the type `Foo` without throwing", ()=>{ 47 | expect(() => { 48 | Foo.validate({x: -7}) 49 | }).not.toThrow(); 50 | }); 51 | 52 | it("validates an object: `{x: 0}` matching the type `Foo` without throwing", ()=>{ 53 | expect(() => { 54 | Foo.validate({x: 0}) 55 | }).not.toThrow(); 56 | }); 57 | 58 | it("throws an error validating an object: `{x: '7'}` not matching the type `Foo`", ()=>{ 59 | expect(() => { 60 | Foo.validate({x: '7'}) 61 | }).toThrow(); 62 | }); 63 | 64 | it("throws an error validating an object: `{y: 7}` not matching the type `Foo`", ()=>{ 65 | expect(() => { 66 | Foo.validate({y: 7}) 67 | }).toThrow(); 68 | }); 69 | 70 | it("throws an error validating a string: `foo` not matching the type `Foo`", ()=>{ 71 | expect(() => { 72 | Foo.validate('foo') 73 | }).toThrow(); 74 | }); 75 | 76 | it("throws an error validating a number: 7 not matching the type `Foo`", ()=>{ 77 | expect(() => { 78 | Foo.validate(7) 79 | }).toThrow(); 80 | }); 81 | 82 | it("throws an error validating a boolean: false not matching the type `Foo`", ()=>{ 83 | expect(() => { 84 | Foo.validate(false) 85 | }).toThrow(); 86 | }); 87 | }) 88 | "#, 89 | )?; 90 | 91 | output.run_ts() 92 | } 93 | 94 | #[test] 95 | fn test_parsing() -> Result<()> { 96 | let output = init_path(SCOPE, "test_parsing"); 97 | 98 | export_types!( 99 | types: [ Foo ], 100 | destinations: [( 101 | output.ts_path(), 102 | // prefix: TESTING_PREFIX, 103 | // postfix: post, 104 | emitters: [ 105 | TypeScript(), 106 | TSValidation(), 107 | TSFormat( 108 | tab_size: 2, 109 | line_width: 80, 110 | ), 111 | ], 112 | )] 113 | )?; 114 | 115 | output.write_jest( 116 | "Foo", 117 | r#" 118 | 119 | describe('Simple Struct Parsing', ()=>{ 120 | it('parses json: `{"x": 7}` matching the type `Foo` without throwing', ()=>{ 121 | expect(() => { 122 | const foo = Foo.parse(`{"x": 7}`); 123 | expect(foo.x).toBe(7); 124 | }).not.toThrow(); 125 | }); 126 | it('parses json: `{"x": -7}` matching the type `Foo` without throwing', ()=>{ 127 | expect(() => { 128 | const foo = Foo.parse(`{"x": -7}`); 129 | expect(foo.x).toBe(-7); 130 | }).not.toThrow(); 131 | }); 132 | it('parses json: `{"x": 0}` matching the type `Foo` without throwing', ()=>{ 133 | expect(() => { 134 | const foo = Foo.parse(`{"x": 0}`); 135 | expect(foo.x).toBe(0); 136 | }).not.toThrow(); 137 | }); 138 | it('parses json: `{"x": 3.14159}` matching the type `Foo` without throwing', ()=>{ 139 | expect(() => { 140 | const foo = Foo.parse(`{"x": 3.14159}`); 141 | expect(foo.x).toBe(3.14159); 142 | }).not.toThrow(); 143 | }); 144 | it('throws an error parsing a string: `{"y": 7}` not matching the type `Foo`', ()=>{ 145 | expect(() => { 146 | Foo.parse(`{"y": 7}`) 147 | }).toThrow(); 148 | }); 149 | it('throws an error parsing a string: `qewcm9823d` not matching the type `Foo`', ()=>{ 150 | expect(() => { 151 | Foo.parse(`qewcm9823d`) 152 | }).toThrow(); 153 | }); 154 | it('throws an error parsing an invalid json string: `{x: 7}` not matching the type `Foo`', ()=>{ 155 | expect(() => { 156 | Foo.parse(`{x: 7}`) 157 | }).toThrow(); 158 | }); 159 | }) 160 | "#, 161 | )?; 162 | 163 | output.run_ts() 164 | } 165 | -------------------------------------------------------------------------------- /type_reflect/tests/test_struct_types.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json; 8 | use type_reflect::*; 9 | 10 | #[derive(Reflect, Serialize, Deserialize)] 11 | pub struct Named { 12 | pub x: u32, 13 | } 14 | 15 | #[derive(Reflect, Serialize, Deserialize)] 16 | pub struct Tuple(u32); 17 | 18 | #[derive(Reflect, Serialize, Deserialize)] 19 | pub struct MultiTuple(u32, Named, Tuple); 20 | 21 | pub const SCOPE: &'static str = "test_struct_types"; 22 | 23 | #[test] 24 | fn test_named() -> Result<()> { 25 | let output = init_path(SCOPE, "test_named"); 26 | 27 | let named = Named { x: 42 }; 28 | let serialized = serde_json::to_string_pretty(&named)?; 29 | eprintln!("Serialzied named: {}", serialized); 30 | 31 | export_types!( 32 | types: [ Named ], 33 | destinations: [( 34 | output.ts_path(), 35 | emitters: [ 36 | TypeScript(), 37 | TSValidation(), 38 | TSFormat( 39 | tab_size: 2, 40 | line_width: 80, 41 | ), 42 | ], 43 | )] 44 | )?; 45 | output.write_jest( 46 | "Named", 47 | ts_string! { 48 | describe("Named Validation", ()=>{ 49 | it("Validates a valid named striuct", ()=>{ 50 | expect(() => { 51 | Named.validate({x:42}) 52 | }).not.toThrow(); 53 | }); 54 | }); 55 | } 56 | .as_str(), 57 | )?; 58 | 59 | output.run_ts() 60 | } 61 | 62 | #[test] 63 | fn test_tuple() -> Result<()> { 64 | let output = init_path(SCOPE, "test_tuple"); 65 | 66 | let tuple = Tuple(42); 67 | let serialized = serde_json::to_string_pretty(&tuple)?; 68 | eprintln!("Serialzied tuple: {}", serialized); 69 | 70 | export_types!( 71 | types: [ Tuple ], 72 | destinations: [( 73 | output.ts_path(), 74 | emitters: [ 75 | TypeScript(), 76 | TSValidation(), 77 | TSFormat( 78 | tab_size: 2, 79 | line_width: 80, 80 | ), 81 | ], 82 | )] 83 | )?; 84 | 85 | output.write_jest( 86 | "Tuple", 87 | ts_string! { 88 | describe("Tuple Validation", ()=>{ 89 | it("Validates a valid tuple", ()=>{ 90 | expect(() => { 91 | Tuple.validate(42) 92 | }).not.toThrow(); 93 | }); 94 | }); 95 | } 96 | .as_str(), 97 | )?; 98 | 99 | output.run_ts() 100 | } 101 | 102 | #[test] 103 | fn test_multi_tuple() -> Result<()> { 104 | let output = init_path(SCOPE, "test_multi_tuple"); 105 | 106 | let tuple = MultiTuple(42, Named { x: 42 }, Tuple(7)); 107 | let serialized = serde_json::to_string_pretty(&tuple)?; 108 | eprintln!("Serialzied tuple: {}", serialized); 109 | 110 | export_types!( 111 | types: [ Named, Tuple, MultiTuple ], 112 | destinations: [( 113 | output.ts_path(), 114 | emitters: [ 115 | TypeScript(), 116 | TSValidation(), 117 | TSFormat( 118 | tab_size: 2, 119 | line_width: 80, 120 | ), 121 | ], 122 | )] 123 | )?; 124 | 125 | output.write_jest( 126 | "Named, Tuple, MultiTuple", 127 | ts_string! { 128 | describe("MultiTuple Validation", ()=>{ 129 | it("Validates a valid multi-tuple", ()=>{ 130 | expect(() => { 131 | MultiTuple.validate([ 132 | 42, 133 | { x: 7 }, 134 | 99 135 | ]) 136 | }).not.toThrow(); 137 | }); 138 | }); 139 | } 140 | .as_str(), 141 | )?; 142 | 143 | output.run_ts() 144 | } 145 | -------------------------------------------------------------------------------- /type_reflect/tests/test_ts_quote.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use ts_quote::ts_quote; 7 | use ts_quote::{TSSource, TS}; 8 | use type_reflect::*; 9 | 10 | pub const SCOPE: &'static str = "test_ts_quote"; 11 | 12 | #[test] 13 | fn test_ident_substitution() -> Result<()> { 14 | let output = init_path(SCOPE, "test_ident_substitution"); 15 | 16 | let hola = 7; 17 | let foo = 3; 18 | let bar = 4; 19 | 20 | let ts: TS = ts_quote! { 21 | const val = #hola + #{foo + bar}; 22 | const lemon = #"`egg salad sandwich ${val}`"; 23 | const peas = #"`egg salad sandwich ${val} == #foo`"; 24 | const soup = #"`egg salad sandwich ${val} == #{foo - bar} something something`"; 25 | }?; 26 | 27 | let prefix = ts.formatted(None)?; 28 | 29 | export_types!( 30 | types: [], 31 | destinations: [( 32 | output.ts_path(), 33 | prefix: prefix, 34 | emitters: [ 35 | TSFormat( 36 | tab_size: 2, 37 | line_width: 80, 38 | ), 39 | ], 40 | )] 41 | )?; 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /type_reflect/tests/test_ts_string.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use ts_quote::ts_string; 7 | use type_reflect::*; 8 | 9 | pub const SCOPE: &'static str = "test_ts_string"; 10 | 11 | #[test] 12 | fn test_ts_str() -> Result<()> { 13 | let output = init_path(SCOPE, "test_ts_string"); 14 | 15 | let prefix = ts_string! { 16 | const x = 4; 17 | }; 18 | 19 | export_types!( 20 | types: [ ], 21 | destinations: [( 22 | output.ts_path(), 23 | prefix: prefix, 24 | emitters: [ 25 | TSFormat( 26 | tab_size: 2, 27 | line_width: 80, 28 | ), 29 | ], 30 | )] 31 | )?; 32 | 33 | Ok(()) 34 | } 35 | 36 | #[test] 37 | fn test_with_str() -> Result<()> { 38 | let output = init_path(SCOPE, "test_with_str"); 39 | 40 | let prefix = ts_string! { 41 | const x = "Foo"; 42 | }; 43 | 44 | export_types!( 45 | types: [ ], 46 | destinations: [( 47 | output.ts_path(), 48 | prefix: prefix, 49 | emitters: [ 50 | TSFormat( 51 | tab_size: 2, 52 | line_width: 80, 53 | ), 54 | ], 55 | )] 56 | )?; 57 | 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn test_groups() -> Result<()> { 63 | let output = init_path(SCOPE, "test_groups"); 64 | 65 | let prefix = ts_string! { 66 | const double = (x: number): number => { 67 | return x * 2; 68 | } 69 | }; 70 | 71 | export_types!( 72 | types: [ ], 73 | destinations: [( 74 | output.ts_path(), 75 | prefix: prefix, 76 | emitters: [ 77 | TSFormat( 78 | tab_size: 2, 79 | line_width: 80, 80 | ), 81 | ], 82 | )] 83 | )?; 84 | 85 | Ok(()) 86 | } 87 | 88 | #[test] 89 | fn test_ident_substitution() -> Result<()> { 90 | let output = init_path(SCOPE, "test_ident_substitution"); 91 | 92 | let hola = 7; 93 | let foo = 3; 94 | let bar = 4; 95 | 96 | let prefix = ts_string! { 97 | const val = #hola + #{foo + bar}; 98 | const lemon = #"`egg salad sandwich ${val}`"; 99 | const peas = #"`egg salad sandwich ${val} == #foo`"; 100 | const soup = #"`egg salad sandwich ${val} == #{foo - bar} something something`"; 101 | }; 102 | 103 | export_types!( 104 | types: [], 105 | destinations: [( 106 | output.ts_path(), 107 | prefix: prefix, 108 | emitters: [ 109 | TSFormat( 110 | tab_size: 2, 111 | line_width: 80, 112 | ), 113 | ], 114 | )] 115 | )?; 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /type_reflect/tests/test_untagged_enum.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use anyhow::Result; 4 | use common::*; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use type_reflect::*; 8 | 9 | #[derive(Serialize, Deserialize, Reflect)] 10 | pub struct Rectangle { 11 | width: f32, 12 | height: f32, 13 | } 14 | 15 | #[derive(Serialize, Deserialize, Reflect)] 16 | #[serde(rename_all = "camelCase")] 17 | pub enum Shape { 18 | Circle { radius: f32 }, 19 | Square { side: f32 }, 20 | Rectangle(Rectangle), 21 | Scale(f32), 22 | ScaledRectangle(Rectangle, f32), 23 | Null, 24 | } 25 | 26 | pub const SCOPE: &'static str = "test_untagged_enum"; 27 | 28 | #[test] 29 | fn test_validation() -> Result<()> { 30 | let output = init_path(SCOPE, "test_validation"); 31 | 32 | let value = Shape::Circle { radius: 5.0 }; 33 | let json = serde_json::to_string_pretty(&value)?; 34 | println!("{json}"); 35 | 36 | let value = Shape::Rectangle(Rectangle { 37 | width: 5.0, 38 | height: 5.0, 39 | }); 40 | let json = serde_json::to_string_pretty(&value)?; 41 | println!("{json}"); 42 | 43 | let value = Shape::ScaledRectangle( 44 | Rectangle { 45 | width: 5.0, 46 | height: 5.0, 47 | }, 48 | 2.0, 49 | ); 50 | let json = serde_json::to_string_pretty(&value)?; 51 | println!("{json}"); 52 | 53 | let value = Shape::Scale(2.0); 54 | let json = serde_json::to_string_pretty(&value)?; 55 | println!("{json}"); 56 | 57 | let value = Shape::Null; 58 | let json = serde_json::to_string_pretty(&value)?; 59 | println!("{json}"); 60 | 61 | export_types!( 62 | types: [Rectangle, Shape], 63 | destinations: [( 64 | output.ts_path(), 65 | emitters: [ 66 | TypeScript(), 67 | TSValidation(), 68 | TSFormat( 69 | tab_size: 2, 70 | line_width: 60, 71 | ), 72 | ], 73 | )] 74 | )?; 75 | 76 | output.write_jest( 77 | "Shape, Rectangle", 78 | ts_string! { 79 | describe("ADT Validation", ()=>{ 80 | it("Validates a Null variant:", ()=>{ 81 | expect(() => { 82 | Shape.validate("null") 83 | }).not.toThrow(); 84 | }); 85 | it("Validates a Circle variant:", ()=>{ 86 | expect(() => { 87 | Shape.validate({ 88 | circle: { 89 | radius: 1.7 90 | } 91 | }) 92 | }).not.toThrow(); 93 | }); 94 | it("Validates a Rectangle variant:", ()=>{ 95 | expect(() => { 96 | Shape.validate({ 97 | rectangle: { 98 | width: 1, 99 | height: 2 100 | } 101 | }) 102 | }).not.toThrow(); 103 | }); 104 | it("Validates a ScaledRectangle variant:", ()=>{ 105 | expect(() => { 106 | Shape.validate({ 107 | 108 | scaledRectangle: [ 109 | { 110 | width: 1, 111 | height: 2 112 | }, 113 | 0.5 114 | ] 115 | }) 116 | }).not.toThrow(); 117 | }); 118 | it("Doesn't Validate an incorrect ScaledRectangle variant:", ()=>{ 119 | expect(() => { 120 | Shape.validate({ 121 | circle: [ 122 | { 123 | width: 1, 124 | height: 2 125 | }, 126 | 0.5 127 | ] 128 | }) 129 | }).toThrow(); 130 | }); 131 | 132 | }); 133 | } 134 | .as_str(), 135 | )?; 136 | 137 | output.run_ts().unwrap(); 138 | 139 | Ok(()) 140 | } 141 | -------------------------------------------------------------------------------- /type_reflect_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "type_reflect_core" 3 | version = "0.5.0" 4 | authors = ["Spencer Kohan "] 5 | edition = "2021" 6 | description = "Utility functions for type_reflect" 7 | license = "Apache-2.0" 8 | 9 | repository = "https://github.com/spencerkohan/type_reflect/tree/main/type_reflect_core" 10 | documentation = "https://docs.rs/type_reflect_core" 11 | 12 | [dependencies] 13 | proc-macro2 = "1.0.69" 14 | quote = "1" 15 | syn = { version = "1", features = ["full", "extra-traits"] } 16 | Inflector = { version = "0.11", default-features = false } 17 | -------------------------------------------------------------------------------- /type_reflect_core/README.md: -------------------------------------------------------------------------------- 1 | # Type Reflect Core 2 | 3 | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](type_reflect_core) [![Crates.io](https://img.shields.io/crates/v/type_reflect_core.svg)](https://crates.io/crates/type_reflect_core) [![Documentation](https://docs.rs/type_reflect_core/badge.svg)](https://docs.rs/type_reflect_core) 4 | 5 | A crate for shared components used by both `type_reflect` and `type_reflect_macros`. 6 | 7 | *This crate is part of a larger workspace, see the [monorepo README](https://github.com/spencerkohan/type_reflect) for more details* 8 | -------------------------------------------------------------------------------- /type_reflect_core/src/inflection.rs: -------------------------------------------------------------------------------- 1 | use crate::syn_err; 2 | pub trait Inflectable { 3 | fn inflect(&self, inflection: Inflection) -> String; 4 | } 5 | 6 | impl Inflectable for &str { 7 | fn inflect(&self, inflection: Inflection) -> String { 8 | inflection.apply(self) 9 | } 10 | } 11 | 12 | impl Inflectable for String { 13 | fn inflect(&self, inflection: Inflection) -> String { 14 | inflection.apply(self.as_str()) 15 | } 16 | } 17 | 18 | #[derive(Copy, Clone, Debug)] 19 | pub enum Inflection { 20 | Lower, 21 | Upper, 22 | Camel, 23 | Snake, 24 | Pascal, 25 | ScreamingSnake, 26 | Kebab, 27 | None, 28 | } 29 | 30 | impl Default for Inflection { 31 | fn default() -> Self { 32 | Self::None 33 | } 34 | } 35 | 36 | impl Inflection { 37 | pub fn apply(self, string: &str) -> String { 38 | use inflector::Inflector; 39 | 40 | match self { 41 | Inflection::Lower => string.to_lowercase(), 42 | Inflection::Upper => string.to_uppercase(), 43 | Inflection::Camel => string.to_camel_case(), 44 | Inflection::Snake => string.to_snake_case(), 45 | Inflection::Pascal => string.to_pascal_case(), 46 | Inflection::ScreamingSnake => string.to_screaming_snake_case(), 47 | Inflection::Kebab => string.to_kebab_case(), 48 | Inflection::None => string.to_string(), 49 | } 50 | } 51 | } 52 | 53 | impl TryFrom for Inflection { 54 | type Error = syn::Error; 55 | 56 | fn try_from(value: String) -> syn::Result { 57 | Ok( 58 | match &*value.to_lowercase().replace("_", "").replace("-", "") { 59 | "lowercase" => Self::Lower, 60 | "uppercase" => Self::Upper, 61 | "camelcase" => Self::Camel, 62 | "snakecase" => Self::Snake, 63 | "pascalcase" => Self::Pascal, 64 | "screamingsnakecase" => Self::ScreamingSnake, 65 | "kebabcase" => Self::Kebab, 66 | _ => syn_err!("invalid inflection: '{}'", value), 67 | }, 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /type_reflect_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod type_description; 2 | pub use type_description::*; 3 | pub mod inflection; 4 | pub use inflection::*; 5 | 6 | #[macro_export] 7 | macro_rules! syn_err { 8 | ($l:literal $(, $a:expr)*) => { 9 | syn_err!(proc_macro2::Span::call_site(); $l $(, $a)*) 10 | }; 11 | ($s:expr; $l:literal $(, $a:expr)*) => { 12 | return Err(syn::Error::new($s, format!($l $(, $a)*))) 13 | }; 14 | } 15 | 16 | #[macro_export] 17 | #[allow(unreachable_code)] 18 | macro_rules! impl_parse { 19 | ($i:ident ($input:ident, $out:ident) { $($k:pat => $e:expr),* $(,)? }) => { 20 | impl std::convert::TryFrom<&syn::Attribute> for $i { 21 | type Error = syn::Error; 22 | fn try_from(attr: &syn::Attribute) -> syn::Result { attr.parse_args() } 23 | } 24 | 25 | impl syn::parse::Parse for $i { 26 | fn parse($input: syn::parse::ParseStream) -> syn::Result { 27 | let mut $out = $i::default(); 28 | loop { 29 | let key: Ident = $input.call(syn::ext::IdentExt::parse_any)?; 30 | 31 | match &*key.to_string() { 32 | $($k => $e,)* 33 | _ => syn_err!($input.span(); "unexpected attribute") 34 | }; 35 | 36 | #[allow(unreachable_code)] 37 | match $input.is_empty() { 38 | true => break, 39 | false => { 40 | $input.parse::()?; 41 | } 42 | } 43 | } 44 | 45 | Ok($out) 46 | } 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /type_reflect_core/src/type_description.rs: -------------------------------------------------------------------------------- 1 | use crate::{Inflectable, Inflection}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct NamedType { 5 | pub name: String, 6 | pub generic_args: Vec>, 7 | } 8 | 9 | #[derive(Clone, Debug)] 10 | pub enum TransparentTypeCase { 11 | Box, 12 | Rc, 13 | Arc, 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct TransparentType { 18 | pub case: TransparentTypeCase, 19 | pub type_: Box, 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | pub enum Type { 24 | Named(NamedType), 25 | String, 26 | Int, 27 | UnsignedInt, 28 | Float, 29 | Boolean, 30 | Transparent(TransparentType), 31 | Option(Box), 32 | Array(Box), 33 | Map { key: Box, value: Box }, 34 | } 35 | 36 | #[derive(Clone, Debug)] 37 | pub struct NamedField { 38 | pub name: String, 39 | pub type_: Type, 40 | } 41 | 42 | #[derive(Clone, Debug)] 43 | pub struct EnumCase { 44 | pub name: String, 45 | pub type_: TypeFieldsDefinition, 46 | pub inflection: Inflection, 47 | } 48 | 49 | impl EnumCase { 50 | pub fn name_with_inflection(&self) -> String { 51 | self.name.inflect(self.inflection) 52 | } 53 | } 54 | 55 | /** 56 | The TypeFieldDefinition represents the set of fields for a type 57 | 58 | This is used both in the context of a struct definition, and for enum variants 59 | */ 60 | #[derive(Clone, Debug)] 61 | pub enum TypeFieldsDefinition { 62 | /** 63 | The Unit field definition describes a type which does not contain data 64 | */ 65 | Unit, 66 | /** 67 | The Tuple field definition describes a type which contains anonymous fields, identified by index 68 | */ 69 | Tuple(Vec), 70 | /** 71 | The Named field definition describes a type which contains named fields, identified by name 72 | */ 73 | Named(Vec), 74 | } 75 | 76 | #[derive(Clone, Debug)] 77 | pub enum EnumType { 78 | Simple, 79 | Complex { 80 | case_key: String, 81 | content_key: Option, 82 | }, 83 | Untagged, 84 | } 85 | 86 | // #[derive(Clone, Debug)] 87 | // pub struct TypeSlot { 88 | // pub optional: bool, 89 | // pub type_: Type, 90 | // } 91 | -------------------------------------------------------------------------------- /type_reflect_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "type_reflect_macros" 3 | version = "0.5.1" 4 | authors = ["Spencer Kohan "] 5 | edition = "2021" 6 | description = "derive macro for type_reflect" 7 | license = "MIT" 8 | 9 | repository = "https://github.com/spencerkohan/type_reflect/tree/main/type_reflect_macros" 10 | documentation = "https://docs.rs/type_reflect_macros" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | type_reflect_core = "0.5.0" 17 | proc-macro2 = "1.0.69" 18 | quote = "1" 19 | syn = { version = "1", features = ["full", "extra-traits"] } 20 | Inflector = { version = "0.11", default-features = false } 21 | -------------------------------------------------------------------------------- /type_reflect_macros/README.md: -------------------------------------------------------------------------------- 1 | # Type Reflect Macros 2 | 3 | [![Github](https://img.shields.io/badge/github-source-blue?logo=github)](type_reflect_macros) [![Crates.io](https://img.shields.io/crates/v/type_reflect_macros.svg)](https://crates.io/crates/type_reflect_macros) [![Documentation](https://docs.rs/type_reflect_macros/badge.svg)](https://docs.rs/type_reflect_macros) 4 | 5 | Procedural macro implementations for `type_reflect`. This crate is for internal use, and the macros are re-exported by the `type_reflect` crate. 6 | 7 | *This crate is part of a larger workspace, see the [monorepo README](https://github.com/spencerkohan/type_reflect) for more details* 8 | -------------------------------------------------------------------------------- /type_reflect_macros/src/attribute_utils.rs: -------------------------------------------------------------------------------- 1 | use syn::parse::{Parse, ParseStream}; 2 | use syn::{Attribute, Ident, Lit, Result, Token}; 3 | pub use type_reflect_core::inflection::*; 4 | use type_reflect_core::{impl_parse, syn_err}; 5 | 6 | #[derive(Default, Clone, Debug)] 7 | pub struct RenameAllAttr { 8 | pub rename_all: Inflection, 9 | } 10 | 11 | impl RenameAllAttr { 12 | pub fn from_attrs(attrs: &[Attribute]) -> Result { 13 | let mut result = Self::default(); 14 | parse_attrs(attrs)?.for_each(|a| result.merge(a)); 15 | parse_serde_attrs::(attrs).for_each(|a| result.merge(a)); 16 | Ok(result) 17 | } 18 | 19 | fn merge(&mut self, RenameAllAttr { rename_all }: RenameAllAttr) { 20 | self.rename_all = rename_all; 21 | } 22 | } 23 | 24 | impl_parse! { 25 | RenameAllAttr(input, out) { 26 | "rename_all" => out.rename_all = parse_assign_inflection(input)?, 27 | } 28 | } 29 | 30 | /// Parse all `#[ts(..)]` attributes from the given slice. 31 | pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result> 32 | where 33 | A: TryFrom<&'a Attribute, Error = syn::Error>, 34 | { 35 | Ok(attrs 36 | .iter() 37 | .filter(|a| a.path.is_ident("ts")) 38 | .map(A::try_from) 39 | .collect::>>()? 40 | .into_iter()) 41 | } 42 | 43 | /// Parse all `#[serde(..)]` attributes from the given slice. 44 | // #[cfg(feature = "serde-compat")] 45 | #[allow(unused)] 46 | pub fn parse_serde_attrs<'a, A: TryFrom<&'a Attribute, Error = syn::Error>>( 47 | attrs: &'a [Attribute], 48 | ) -> impl Iterator { 49 | attrs 50 | .iter() 51 | .filter(|a| a.path.is_ident("serde")) 52 | .flat_map(|attr| match A::try_from(attr) { 53 | Ok(attr) => Some(attr), 54 | Err(_) => { 55 | use quote::ToTokens; 56 | // warning::print_warning( 57 | // "failed to parse serde attribute", 58 | // format!("{}", attr.to_token_stream()), 59 | // "ts-rs failed to parse this attribute. It will be ignored.", 60 | // ) 61 | // .unwrap(); 62 | None 63 | } 64 | }) 65 | .collect::>() 66 | .into_iter() 67 | } 68 | 69 | pub fn parse_assign_str(input: ParseStream) -> Result { 70 | input.parse::()?; 71 | match Lit::parse(input)? { 72 | Lit::Str(string) => Ok(string.value()), 73 | other => Err(syn::Error::new(other.span(), "expected string")), 74 | } 75 | } 76 | 77 | pub fn parse_assign_inflection(input: ParseStream) -> Result { 78 | match parse_assign_str(input) { 79 | Ok(str) => Inflection::try_from(str), 80 | Err(_) => Ok(Inflection::None), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /type_reflect_macros/src/export_types_impl/destination.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::*; 2 | use quote::*; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::token::Paren; 5 | use syn::*; 6 | 7 | use super::{peak_arg_name, DestinationArg, NamedArg}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub enum Destination { 11 | Named(NamedDestination), 12 | Unnamed(UnnamedDestination), 13 | } 14 | 15 | impl Parse for Destination { 16 | fn parse(input: ParseStream) -> Result { 17 | if input.peek(syn::token::Paren) { 18 | let dest = input.parse()?; 19 | return Ok(Destination::Unnamed(dest)); 20 | } 21 | let dest = input.parse()?; 22 | return Ok(Destination::Named(dest)); 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct NamedDestination { 28 | pub export_type: Expr, 29 | pub destinations: Vec, 30 | pub named_args: Vec, 31 | pub prefix: Option, 32 | pub postfix: Option, 33 | } 34 | 35 | impl Parse for NamedDestination { 36 | fn parse(input: ParseStream) -> Result { 37 | let mut export_type_tokens: TokenStream = quote! {}; 38 | 39 | while !input.peek(syn::token::Paren) && !input.is_empty() { 40 | let next: TokenTree = input.parse()?; 41 | export_type_tokens.append(next); 42 | } 43 | 44 | let export_type: Expr = syn::parse2(export_type_tokens)?; 45 | 46 | let content; 47 | let _parens: Paren = parenthesized!(content in input); 48 | 49 | let mut args: Vec = vec![]; 50 | 51 | while !content.is_empty() { 52 | let arg: DestinationArg = content.parse()?; 53 | args.push(arg); 54 | if content.peek(Token![,]) { 55 | let _comma: Token![,] = content.parse()?; 56 | } 57 | } 58 | 59 | let mut named_args: Vec = vec![]; 60 | 61 | let destinations: Vec = args 62 | .into_iter() 63 | .filter_map(|arg| match arg { 64 | DestinationArg::Dest(expr) => Some(expr), 65 | DestinationArg::Named(arg) => { 66 | named_args.push(arg); 67 | None 68 | } 69 | }) 70 | .collect(); 71 | 72 | let mut prefix: Option = None; 73 | let mut postfix: Option = None; 74 | let named_args = named_args 75 | .into_iter() 76 | .filter(|arg| { 77 | match arg.name().as_str() { 78 | "prefix" => { 79 | prefix = Some(arg.expr.clone()); 80 | return false; 81 | } 82 | "postfix" => { 83 | postfix = Some(arg.expr.clone()); 84 | return false; 85 | } 86 | _ => {} 87 | }; 88 | true 89 | }) 90 | .collect(); 91 | 92 | Ok(Self { 93 | export_type, 94 | destinations, 95 | named_args, 96 | prefix, 97 | postfix, 98 | }) 99 | } 100 | } 101 | 102 | #[derive(Debug, Clone)] 103 | pub struct EmitterDecl { 104 | pub type_name: Expr, 105 | pub args: Vec, 106 | } 107 | 108 | impl Parse for EmitterDecl { 109 | fn parse(input: ParseStream) -> Result { 110 | let mut export_type_tokens: TokenStream = quote! {}; 111 | 112 | while !input.peek(syn::token::Paren) && !input.is_empty() { 113 | let next: TokenTree = input.parse()?; 114 | export_type_tokens.append(next); 115 | } 116 | 117 | let type_name: Expr = syn::parse2(export_type_tokens)?; 118 | 119 | let content; 120 | let _parens: Paren = parenthesized!(content in input); 121 | 122 | let mut args: Vec = vec![]; 123 | 124 | while !content.is_empty() { 125 | let arg: NamedArg = content.parse()?; 126 | args.push(arg); 127 | if content.peek(Token![,]) { 128 | let _comma: Token![,] = content.parse()?; 129 | } 130 | } 131 | 132 | Ok(Self { type_name, args }) 133 | } 134 | } 135 | 136 | #[derive(Debug, Clone)] 137 | pub struct EmitterDeclList { 138 | pub emitters: Vec, 139 | } 140 | 141 | impl Parse for EmitterDeclList { 142 | fn parse(input: ParseStream) -> Result { 143 | let content; 144 | let _parens: token::Bracket = bracketed!(content in input); 145 | let mut emitters: Vec = vec![]; 146 | 147 | while !content.is_empty() { 148 | let emitter: EmitterDecl = content.parse()?; 149 | emitters.push(emitter); 150 | if content.peek(Token![,]) { 151 | let _comma: Token![,] = content.parse()?; 152 | } 153 | } 154 | 155 | Ok(Self { emitters }) 156 | } 157 | } 158 | 159 | #[derive(Debug, Clone)] 160 | pub struct UnnamedDestination { 161 | pub destinations: Vec, 162 | pub prefix: Option, 163 | pub postfix: Option, 164 | pub emitters: Vec, 165 | } 166 | 167 | impl Parse for UnnamedDestination { 168 | fn parse(input: ParseStream) -> Result { 169 | let content; 170 | let _parens: Paren = parenthesized!(content in input); 171 | 172 | // let mut args: Vec = vec![]; 173 | 174 | let mut destinations: Vec = vec![]; 175 | let mut prefix: Option = None; 176 | let mut postfix: Option = None; 177 | let mut emitters: EmitterDeclList = EmitterDeclList { emitters: vec![] }; 178 | 179 | while !content.is_empty() { 180 | match peak_arg_name(&&content) { 181 | Some(name) => match name.to_string().as_str() { 182 | "prefix" => { 183 | let _: Ident = content.parse()?; 184 | let _: Token![:] = content.parse()?; 185 | prefix = Some(content.parse()?); 186 | } 187 | "postfix" => { 188 | let _: Ident = content.parse()?; 189 | let _: Token![:] = content.parse()?; 190 | postfix = Some(content.parse()?); 191 | } 192 | "emitters" => { 193 | let _: Ident = content.parse()?; 194 | let _: Token![:] = content.parse()?; 195 | emitters = content.parse()?; 196 | } 197 | _other => { 198 | // TODO: this should produce an error 199 | } 200 | }, 201 | None => { 202 | let dest: Expr = content.parse()?; 203 | destinations.push(dest); 204 | } 205 | } 206 | if content.peek(Token![,]) { 207 | let _comma: Token![,] = content.parse()?; 208 | } 209 | } 210 | 211 | Ok(Self { 212 | destinations, 213 | prefix, 214 | postfix, 215 | emitters: emitters.emitters, 216 | }) 217 | } 218 | } 219 | 220 | pub fn emit_destination(dest: &Destination, types: &Vec<&Ident>) -> TokenStream { 221 | match dest { 222 | Destination::Named(dest) => emit_named_destination(dest, types), 223 | Destination::Unnamed(dest) => emit_unnamed_destination(dest, types), 224 | } 225 | } 226 | 227 | pub fn emit_named_destination(dest: &NamedDestination, types: &Vec<&Ident>) -> TokenStream { 228 | let emitter = &dest.export_type; 229 | 230 | let prefix = match &dest.prefix { 231 | Some(expr) => { 232 | quote! { #expr } 233 | } 234 | None => quote! { "" }, 235 | }; 236 | 237 | let postfix = &dest.postfix; 238 | 239 | let emitter_args = &dest.named_args; 240 | let emitter_args = quote! { #(#emitter_args,)* }; 241 | 242 | let mut result = quote! {}; 243 | for dest in &dest.destinations { 244 | result.extend(quote! { 245 | let mut file = type_reflect::init_destination_file(#dest, #prefix)?; 246 | let mut emitter = #emitter { 247 | #emitter_args 248 | ..Default::default() 249 | }; 250 | file.write_all(emitter.prefix().as_bytes())?; 251 | }); 252 | for type_ in types { 253 | result.extend(quote! { 254 | file.write_all(emitter.emit::<#type_>().as_bytes())?; 255 | }); 256 | } 257 | result.extend(quote! { 258 | emitter.finalize(#dest)?; 259 | }); 260 | match postfix { 261 | Some(expr) => result.extend(quote! { 262 | type_reflect::write_postfix(#dest, #expr)?; 263 | }), 264 | None => {} 265 | }; 266 | } 267 | result 268 | } 269 | 270 | pub fn emit_single_emitter(emitter: &EmitterDecl, types: &Vec<&Ident>, dest: &Expr) -> TokenStream { 271 | let emitter_name = &emitter.type_name; 272 | 273 | let emitter_args = &emitter.args; 274 | let emitter_args = quote! { #(#emitter_args,)* }; 275 | 276 | let mut result = quote! {}; 277 | result.extend(quote! { 278 | let mut emitter = #emitter_name { 279 | #emitter_args 280 | ..Default::default() 281 | }; 282 | file.write_all(emitter.prefix().as_bytes())?; 283 | }); 284 | for type_ in types { 285 | result.extend(quote! { 286 | file.write_all(emitter.emit::<#type_>().as_bytes())?; 287 | }); 288 | } 289 | result.extend(quote! { 290 | emitter.finalize(#dest)?; 291 | }); 292 | 293 | result 294 | } 295 | 296 | pub fn emit_unnamed_destination(dest: &UnnamedDestination, types: &Vec<&Ident>) -> TokenStream { 297 | let prefix = match &dest.prefix { 298 | Some(expr) => { 299 | quote! { #expr } 300 | } 301 | None => quote! { "" }, 302 | }; 303 | 304 | let postfix = &dest.postfix; 305 | 306 | let emitters = &dest.emitters; 307 | 308 | let mut result = quote! {}; 309 | for dest in &dest.destinations { 310 | result.extend(quote! { 311 | let mut file = type_reflect::init_destination_file(#dest, #prefix)?; 312 | }); 313 | for emitter in emitters { 314 | result.extend(quote! { 315 | let mut file = std::fs::OpenOptions::new() 316 | .write(true) 317 | .append(true) 318 | .open(#dest)?; 319 | }); 320 | result.extend(emit_single_emitter(emitter, types, dest)); 321 | } 322 | match postfix { 323 | Some(expr) => result.extend(quote! { 324 | type_reflect::write_postfix(#dest, #expr)?; 325 | }), 326 | None => {} 327 | }; 328 | } 329 | result 330 | } 331 | -------------------------------------------------------------------------------- /type_reflect_macros/src/export_types_impl/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::*; 2 | use quote::*; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::punctuated::*; 5 | use syn::token::Bracket; 6 | use syn::*; 7 | mod destination; 8 | use destination::*; 9 | 10 | #[derive(Debug, Clone)] 11 | struct ItemsList { 12 | idents: Punctuated, 13 | } 14 | 15 | impl ItemsList { 16 | fn args(&self) -> Vec<&Ident> { 17 | (&self.idents).into_iter().collect() 18 | } 19 | } 20 | 21 | impl Parse for ItemsList { 22 | fn parse(input: ParseStream) -> Result { 23 | let ident: Ident = input.parse()?; 24 | if ident.to_string().as_str() != "types" { 25 | return Err(syn::Error::new( 26 | ident.span(), 27 | r#"Expected argument name: "types""#, 28 | )); 29 | } 30 | let _colon_token: Token![:] = input.parse()?; 31 | let content; 32 | let _brackets: Bracket = bracketed!(content in input); 33 | let idents = content.parse_terminated(Ident::parse)?; 34 | Ok(Self { idents }) 35 | } 36 | } 37 | 38 | #[derive(Debug, Clone)] 39 | struct DestinationList { 40 | destinations: Vec, 41 | } 42 | 43 | impl Parse for DestinationList { 44 | fn parse(input: ParseStream) -> Result { 45 | let ident: Ident = input.parse()?; 46 | if ident.to_string().as_str() != "destinations" { 47 | return Err(syn::Error::new( 48 | ident.span(), 49 | r#"Expected argument name: "destinations""#, 50 | )); 51 | } 52 | 53 | let _colon_token: Token![:] = input.parse()?; 54 | let content; 55 | let _brackets: Bracket = bracketed!(content in input); 56 | let destinations: Punctuated = 57 | match content.parse_terminated(Destination::parse) { 58 | Ok(res) => res, 59 | Err(err) => { 60 | return Err(syn::Error::new( 61 | err.span(), 62 | format!("Error parsing destinations list: {}", err), 63 | )); 64 | } 65 | }; 66 | 67 | let destinations: Vec = destinations.into_iter().map(|dest| dest).collect(); 68 | 69 | Ok(Self { destinations }) 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone)] 74 | pub struct NamedArg { 75 | ident: Ident, 76 | expr: Expr, 77 | } 78 | 79 | impl NamedArg { 80 | pub fn name(&self) -> String { 81 | self.ident.to_string() 82 | } 83 | } 84 | 85 | impl ToTokens for NamedArg { 86 | fn to_tokens(&self, tokens: &mut TokenStream) { 87 | let ident = &self.ident; 88 | let expr = &self.expr; 89 | tokens.extend(quote! { #ident: #expr }) 90 | } 91 | } 92 | 93 | impl Parse for NamedArg { 94 | fn parse(input: ParseStream) -> Result { 95 | let ident: Ident = input.parse()?; 96 | 97 | let _colon_token: Token![:] = input.parse()?; 98 | let expr: Expr = input.parse()?; 99 | 100 | Ok(Self { ident, expr }) 101 | } 102 | } 103 | 104 | #[derive(Debug, Clone)] 105 | pub enum DestinationArg { 106 | Dest(Expr), 107 | Named(NamedArg), 108 | } 109 | 110 | pub fn peak_arg_name(input: &syn::parse::ParseStream) -> Option { 111 | let lookahead = input.lookahead1(); 112 | if lookahead.peek(Ident) { 113 | let forked = input.fork(); 114 | let ident: Ident = match forked.parse::() { 115 | Ok(ident) => ident, 116 | Err(err) => { 117 | eprintln!("Failed to get ident"); 118 | panic!("{}", err); 119 | } 120 | }; 121 | if forked.parse::().is_ok() && !forked.lookahead1().peek(Token![:]) { 122 | // !forked.lookahead1().peek(Ident) { 123 | // We are fairly certain it's a KeyValuePair now 124 | return Some(ident); 125 | } 126 | } 127 | None 128 | } 129 | 130 | impl Parse for DestinationArg { 131 | fn parse(input: syn::parse::ParseStream) -> Result { 132 | let lookahead = input.lookahead1(); 133 | 134 | if lookahead.peek(Ident) { 135 | let forked = input.fork(); 136 | let _ident: Ident = forked.parse()?; 137 | if forked.parse::().is_ok() && !forked.lookahead1().peek(Ident) { 138 | // We are fairly certain it's a KeyValuePair now 139 | let prefix = input.parse::()?; 140 | return Ok(DestinationArg::Named(prefix)); 141 | } 142 | } 143 | let expr: Expr = input.parse()?; 144 | Ok(DestinationArg::Dest(expr)) 145 | } 146 | } 147 | 148 | #[derive(Debug, Clone)] 149 | struct Input { 150 | items: ItemsList, 151 | destinations: DestinationList, 152 | } 153 | 154 | impl Parse for Input { 155 | fn parse(input: ParseStream) -> Result { 156 | let items = input.parse()?; 157 | let _comma_token: Token![,] = input.parse()?; 158 | let destinations = input.parse()?; 159 | Ok(Self { 160 | items, 161 | destinations, 162 | }) 163 | } 164 | } 165 | 166 | pub fn export_types_impl(input: proc_macro::TokenStream) -> Result { 167 | // println!("EXPORT TYPES input: {:#?}", input); 168 | let input = syn::parse::(input)?; 169 | // println!("parse result: {:#?}", input); 170 | 171 | let types = input.items.args(); 172 | let destinations = input.destinations.destinations; 173 | 174 | let mut result = quote! {}; 175 | for dest in destinations { 176 | result.extend(emit_destination(&dest, &types)) 177 | } 178 | 179 | let result = quote! { 180 | (|| -> Result<(), std::io::Error> { 181 | #result 182 | Ok(()) 183 | })() 184 | }; 185 | 186 | // println!("Emitting: {}", result); 187 | // Ok(input) 188 | Ok(result) 189 | } 190 | -------------------------------------------------------------------------------- /type_reflect_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(incomplete_features)] 2 | #![feature(specialization)] 3 | #![macro_use] 4 | // #![deny(unused)] 5 | 6 | use proc_macro2::TokenStream; 7 | use syn::{spanned::Spanned, Item, Result}; 8 | 9 | mod type_def; 10 | use type_def::*; 11 | mod attribute_utils; 12 | 13 | mod export_types_impl; 14 | use export_types_impl::*; 15 | use type_reflect_core::syn_err; 16 | 17 | #[macro_use] 18 | mod utils; 19 | 20 | /// Derives [TS](./trait.TS.html) for a struct or enum. 21 | /// Please take a look at [TS](./trait.TS.html) for documentation. 22 | #[proc_macro_derive(Reflect, attributes(reflect))] 23 | pub fn reflect(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 24 | match entry(input) { 25 | Err(err) => err.to_compile_error(), 26 | Ok(result) => result, 27 | } 28 | .into() 29 | } 30 | 31 | fn entry(input: proc_macro::TokenStream) -> Result { 32 | let input = syn::parse::(input)?; 33 | 34 | // Access the attributes of the input item 35 | 36 | let (type_def, _ident, _generics) = match input { 37 | Item::Struct(s) => { 38 | // println!("Parsed Item::Struct: {:#?}", s); 39 | (TypeDef::struct_def(&s)?, s.ident, s.generics) 40 | } 41 | Item::Enum(e) => { 42 | // println!("Parsed Item::Enum: {:#?}", e); 43 | (TypeDef::enum_def(&e)?, e.ident, e.generics) 44 | } 45 | Item::Type(t) => (TypeDef::alias_def(&t)?, t.ident, t.generics), 46 | _ => { 47 | syn_err!(input.span(); "Item is not supported by the Reflect macro") 48 | } 49 | }; 50 | 51 | // println!("Type Def Parsed: {:#?}", type_def); 52 | // println!("Type Def Emits: \n{}", type_def.emit()); 53 | 54 | Ok(type_def.emit()) 55 | 56 | // Ok(ts.into_impl(ident, generics)) 57 | } 58 | 59 | #[proc_macro] 60 | pub fn export_types(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 61 | match export_types_impl(input) { 62 | Err(err) => err.to_compile_error(), 63 | Ok(result) => result, 64 | } 65 | .into() 66 | } 67 | -------------------------------------------------------------------------------- /type_reflect_macros/src/type_def/enum_def.rs: -------------------------------------------------------------------------------- 1 | use crate::attribute_utils::*; 2 | use crate::type_def::InflectionTokenProvider; 3 | // use crate::utils::*; 4 | use type_reflect_core::EnumType; 5 | use type_reflect_core::Inflection; 6 | 7 | use super::{syn_type_utils::*, type_utils::TypeFieldsDefinitionBridge, RustTypeEmitter}; 8 | use proc_macro2::{Ident, TokenStream}; 9 | use quote::quote; 10 | use syn::{ 11 | // parse::{Parse, ParseStream}, 12 | Attribute, 13 | ItemEnum, 14 | Result, 15 | }; 16 | use type_reflect_core::*; 17 | 18 | #[derive(Clone, Debug)] 19 | pub struct EnumDef { 20 | pub tokens: TokenStream, 21 | pub ident: Ident, 22 | pub enum_type: EnumType, 23 | pub inflection: Inflection, 24 | pub cases: Vec, 25 | } 26 | 27 | fn extract_cases(item: &ItemEnum) -> Result> { 28 | (&item.variants) 29 | .into_iter() 30 | .map(|case| { 31 | let name = format!("{}", case.ident); 32 | let inflection: Inflection = match RenameAllAttr::from_attrs(&case.attrs) { 33 | Err(e) => { 34 | eprintln!( 35 | "Error extracting inflection: {} from attributes: {:#?}", 36 | e, &case.attrs 37 | ); 38 | Inflection::None 39 | } 40 | Ok(rename_all) => { 41 | // println!( 42 | // "Extracted inflection: {:?} from attributes: {:#?}", 43 | // rename_all, &case.attrs 44 | // ); 45 | rename_all.rename_all 46 | } 47 | }; 48 | Ok(EnumCase { 49 | name, 50 | type_: (&case.fields).to_fields()?, 51 | inflection, 52 | }) 53 | }) 54 | .collect() 55 | } 56 | 57 | impl EnumDef { 58 | pub fn new(item: &ItemEnum) -> Result { 59 | let attributes = EnumAttr::from_attrs(&item.attrs)?; 60 | let rename_attr = RenameAllAttr::from_attrs(&item.attrs)?; 61 | 62 | let cases = extract_cases(&item)?; 63 | 64 | let enum_type = match (&cases).into_iter().fold(false, |input, case| { 65 | input 66 | || if let TypeFieldsDefinition::Unit = case.type_ { 67 | false 68 | } else { 69 | true 70 | } 71 | }) { 72 | // false indicates it is not complex 73 | false => EnumType::Simple, 74 | // true indicates the type is complex 75 | true => match attributes.tag { 76 | Some(case_key) => { 77 | let content_key = attributes.content; 78 | EnumType::Complex { 79 | case_key, 80 | content_key, 81 | } 82 | } 83 | None => EnumType::Untagged, 84 | }, 85 | }; 86 | 87 | Ok(Self { 88 | tokens: quote! { #item }, 89 | ident: item.ident.clone(), 90 | enum_type, 91 | inflection: rename_attr.rename_all, 92 | cases, 93 | }) 94 | } 95 | 96 | pub fn emit_cases(&self) -> TokenStream { 97 | let cases: Vec = (&self.cases) 98 | .into_iter() 99 | .map(|case| { 100 | let name = &case.name; 101 | let type_ = case.type_.emit_def(); 102 | let rename_all = &case.inflection.to_tokens(); 103 | quote! { 104 | EnumCase { 105 | name: #name.to_string(), 106 | type_: #type_, 107 | inflection: #rename_all, 108 | } 109 | } 110 | }) 111 | .collect(); 112 | quote! { 113 | #(#cases),* 114 | } 115 | } 116 | 117 | pub fn emit(&self) -> TokenStream { 118 | let ident = &self.ident(); 119 | let name_literal = format!("{}", ident); 120 | let cases = &self.emit_cases(); 121 | let rust = format!("{}", self.tokens()); 122 | 123 | let enum_type = match &self.enum_type { 124 | EnumType::Simple => quote! {EnumType::Simple}, 125 | EnumType::Complex { 126 | case_key, 127 | content_key, 128 | } => match content_key { 129 | Some(content_key) => quote! { 130 | EnumType::Complex { 131 | case_key: #case_key.to_string(), 132 | content_key: Some(#content_key.to_string()) 133 | } 134 | }, 135 | None => quote! { 136 | EnumType::Complex { case_key: #case_key.to_string(), content_key: None } 137 | }, 138 | }, 139 | EnumType::Untagged => quote! { EnumType::Untagged }, 140 | }; 141 | 142 | let inflection = &self.inflection.to_tokens(); 143 | 144 | quote! { 145 | 146 | impl Emittable for #ident { 147 | fn emit_with(emitter: &mut E) -> String { 148 | emitter.emit_enum::() 149 | } 150 | } 151 | 152 | impl EnumReflectionType for #ident { 153 | fn name() -> &'static str { 154 | #name_literal 155 | } 156 | fn inflection() -> Inflection { 157 | #inflection 158 | } 159 | fn enum_type() -> EnumType { 160 | #enum_type 161 | } 162 | fn cases() -> Vec { 163 | vec![ 164 | #cases 165 | ] 166 | } 167 | fn rust() -> String { 168 | #rust.to_string() 169 | } 170 | } 171 | 172 | } 173 | } 174 | } 175 | 176 | impl RustTypeEmitter for EnumDef { 177 | fn ident(&self) -> &Ident { 178 | &self.ident 179 | } 180 | fn tokens(&self) -> &TokenStream { 181 | &self.tokens 182 | } 183 | } 184 | 185 | #[derive(Default, Clone, Debug)] 186 | pub struct EnumAttr { 187 | // pub rename_all: Option, 188 | // pub rename: Option, 189 | // pub export_to: Option, 190 | // pub export: bool, 191 | tag: Option, 192 | // untagged: bool, 193 | content: Option, 194 | } 195 | 196 | #[derive(Default)] 197 | pub struct SerdeEnumAttr(EnumAttr); 198 | 199 | impl EnumAttr { 200 | // pub fn tagged(&self) -> Result> { 201 | // match (false, &self.tag, &self.content) { 202 | // (false, None, None) => Ok(Tagged::Externally), 203 | // (false, Some(tag), None) => Ok(Tagged::Internally { tag }), 204 | // (false, Some(tag), Some(content)) => Ok(Tagged::Adjacently { tag, content }), 205 | // (true, None, None) => Ok(Tagged::Untagged), 206 | // (true, Some(_), None) => syn_err!("untagged cannot be used with tag"), 207 | // (true, _, Some(_)) => syn_err!("untagged cannot be used with content"), 208 | // (false, None, Some(_)) => syn_err!("content cannot be used without tag"), 209 | // } 210 | // } 211 | 212 | pub fn from_attrs(attrs: &[Attribute]) -> Result { 213 | let mut result = Self::default(); 214 | parse_attrs(attrs)?.for_each(|a| result.merge(a)); 215 | // #[cfg(feature = "serde-compat")] 216 | parse_serde_attrs::(attrs).for_each(|a| result.merge(a.0)); 217 | Ok(result) 218 | } 219 | 220 | fn merge( 221 | &mut self, 222 | EnumAttr { 223 | // rename_all, 224 | // rename, 225 | tag, 226 | content, 227 | // untagged, 228 | // export_to, 229 | // export, 230 | }: EnumAttr, 231 | ) { 232 | // self.rename = self.rename.take().or(rename); 233 | // self.rename_all = self.rename_all.take().or(rename_all); 234 | self.tag = self.tag.take().or(tag); 235 | // self.untagged = self.untagged || untagged; 236 | self.content = self.content.take().or(content); 237 | // self.export = self.export || export; 238 | // self.export_to = self.export_to.take().or(export_to); 239 | } 240 | } 241 | 242 | impl_parse! { 243 | EnumAttr(_input, _out) { 244 | // "rename" => out.rename = Some(parse_assign_str(input)?), 245 | // "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), 246 | // "export_to" => out.export_to = Some(parse_assign_str(input)?), 247 | // "export" => out.export = true 248 | } 249 | } 250 | 251 | impl_parse! { 252 | SerdeEnumAttr(input, out) { 253 | // "rename" => out.0.rename = Some(parse_assign_str(input)?), 254 | // "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), 255 | "tag" => out.0.tag = Some(parse_assign_str(input)?), 256 | "content" => out.0.content = Some(parse_assign_str(input)?), 257 | // "untagged" => out.0.untagged = true 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /type_reflect_macros/src/type_def/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, TokenStream}; 2 | use quote::quote; 3 | use syn::{ItemEnum, ItemStruct, ItemType, Result}; 4 | 5 | mod enum_def; 6 | mod struct_def; 7 | mod type_alias_def; 8 | pub use type_alias_def::*; 9 | 10 | pub mod syn_type_utils; 11 | 12 | pub mod type_utils; 13 | 14 | pub use enum_def::*; 15 | pub use struct_def::StructDef; 16 | use type_reflect_core::Inflection; 17 | 18 | #[derive(Clone, Debug)] 19 | pub enum TypeDef { 20 | Struct(StructDef), 21 | Enum(EnumDef), 22 | Alias(TypeAliasDef), 23 | } 24 | 25 | impl TypeDef { 26 | pub fn struct_def(item: &ItemStruct) -> Result { 27 | Ok(TypeDef::Struct(StructDef::new(item)?)) 28 | } 29 | pub fn alias_def(item: &ItemType) -> Result { 30 | Ok(TypeDef::Alias(TypeAliasDef::new(item)?)) 31 | } 32 | pub fn enum_def(item: &ItemEnum) -> Result { 33 | // println!("ATTRIBUTES:"); 34 | // for attr in &item.attrs { 35 | // println!(" {:?}", attr); 36 | // } 37 | Ok(TypeDef::Enum(EnumDef::new(item)?)) 38 | } 39 | 40 | pub fn emit(&self) -> TokenStream { 41 | match self { 42 | TypeDef::Struct(s) => s.emit(), 43 | TypeDef::Enum(e) => e.emit(), 44 | TypeDef::Alias(t) => t.emit(), 45 | } 46 | } 47 | } 48 | 49 | impl RustTypeEmitter for TypeDef { 50 | fn ident(&self) -> &Ident { 51 | panic!("unimplemented") 52 | } 53 | fn tokens(&self) -> &TokenStream { 54 | panic!("unimplemented") 55 | } 56 | // fn emit_type_def_impl(&self) -> TokenStream { 57 | // match self { 58 | // TypeDef::Struct(s) => s.emit_type_def_impl(), 59 | // TypeDef::Enum(e) => e.emit_type_def_impl(), 60 | // TypeDef::Alias(_) => todo!(), 61 | // } 62 | // } 63 | } 64 | 65 | pub trait RustTypeEmitter { 66 | fn ident(&self) -> &Ident; 67 | fn tokens(&self) -> &TokenStream; 68 | // fn emit_type_def_impl(&self) -> TokenStream { 69 | // let ident = &self.ident(); 70 | // let token_string = format!("{}", self.tokens()); 71 | // quote! { 72 | // impl RustType for #ident { 73 | // fn emit_rust(&self) -> String { 74 | // #token_string.to_string() 75 | // } 76 | // } 77 | // } 78 | // } 79 | } 80 | 81 | pub trait InflectionTokenProvider { 82 | fn inflection(&self) -> &Inflection; 83 | fn to_tokens(&self) -> TokenStream { 84 | match &self.inflection() { 85 | Inflection::Lower => quote!(Inflection::Lower), 86 | Inflection::Upper => quote!(Inflection::Upper), 87 | Inflection::Camel => quote!(Inflection::Camel), 88 | Inflection::Snake => quote!(Inflection::Snake), 89 | Inflection::Pascal => quote!(Inflection::Pascal), 90 | Inflection::ScreamingSnake => quote!(Inflection::ScreamingSnake), 91 | Inflection::Kebab => quote!(Inflection::Kebab), 92 | Inflection::None => quote!(Inflection::None), 93 | } 94 | } 95 | } 96 | 97 | impl InflectionTokenProvider for Inflection { 98 | fn inflection(&self) -> &Inflection { 99 | self 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /type_reflect_macros/src/type_def/struct_def.rs: -------------------------------------------------------------------------------- 1 | use super::syn_type_utils::*; 2 | use super::type_utils::*; 3 | use super::InflectionTokenProvider; 4 | use super::RustTypeEmitter; 5 | use crate::attribute_utils::RenameAllAttr; 6 | use proc_macro2::{Ident, TokenStream}; 7 | use quote::quote; 8 | use syn::{ItemStruct, Result}; 9 | use type_reflect_core::Inflection; 10 | use type_reflect_core::TypeFieldsDefinition; 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct StructDef { 14 | tokens: TokenStream, 15 | inflection: Inflection, 16 | ident: Ident, 17 | fields: TypeFieldsDefinition, 18 | } 19 | 20 | // fn extract_members(item: &ItemStruct) -> Result { 21 | // match &(item.fields) { 22 | // syn::Fields::Named(fields) => (&fields).to_named_fields(), 23 | // syn::Fields::Unnamed(fieldsUnnamed) => todo!(), 24 | // syn::Fields::Unit => todo!(), 25 | // } 26 | // } 27 | 28 | impl StructDef { 29 | pub fn new(item: &ItemStruct) -> Result { 30 | let rename_attr = RenameAllAttr::from_attrs(&item.attrs)?; 31 | Ok(Self { 32 | tokens: quote! { #item }, 33 | inflection: rename_attr.rename_all, 34 | ident: item.ident.clone(), 35 | fields: (&item.fields).to_fields()?, 36 | }) 37 | } 38 | 39 | pub fn emit_fields(&self) -> TokenStream { 40 | // let members: Vec = (&self.fields) 41 | // .into_iter() 42 | // .map(|member| member.emit_member()) 43 | // .collect(); 44 | // quote! { 45 | // #(#members),* 46 | // } 47 | 48 | return (&self.fields).emit_def(); 49 | } 50 | 51 | pub fn emit(&self) -> TokenStream { 52 | let ident = &self.ident(); 53 | let name_literal = format!("{}", ident); 54 | let members = &self.emit_fields(); 55 | let rust = format!("{}", self.tokens()); 56 | let inflection = &self.inflection.to_tokens(); 57 | quote! { 58 | 59 | impl Emittable for #ident { 60 | fn emit_with(emitter: &mut E) -> String { 61 | emitter.emit_struct::() 62 | } 63 | } 64 | 65 | impl StructType for #ident { 66 | fn name() -> &'static str { 67 | #name_literal 68 | } 69 | fn inflection() -> Inflection { 70 | #inflection 71 | } 72 | fn fields() -> TypeFieldsDefinition { 73 | #members 74 | } 75 | fn rust() -> String { 76 | #rust.to_string() 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | impl RustTypeEmitter for StructDef { 84 | fn ident(&self) -> &Ident { 85 | &self.ident 86 | } 87 | fn tokens(&self) -> &TokenStream { 88 | &self.tokens 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /type_reflect_macros/src/type_def/syn_type_utils.rs: -------------------------------------------------------------------------------- 1 | use syn::{Field, GenericArgument, PathArguments, Result, Type as SynType, TypePath}; 2 | use type_reflect_core::{ 3 | syn_err, NamedField, NamedType, TransparentType, TransparentTypeCase, Type, 4 | TypeFieldsDefinition, 5 | }; 6 | 7 | fn leading_segment(path: &TypePath) -> String { 8 | path.path.segments[0].ident.to_string() 9 | } 10 | 11 | fn generic_args(path: &TypePath) -> Result> { 12 | match &path.path.segments[0].arguments { 13 | PathArguments::None => Ok(vec![]), 14 | PathArguments::AngleBracketed(args) => (&args.args) 15 | .into_iter() 16 | .map(|arg| match arg { 17 | GenericArgument::Type(inner_ty) => inner_ty.to_type(), 18 | _ => syn_err!("Generic argument must be a type: {:#?}", arg), 19 | }) 20 | .collect(), 21 | _ => syn_err!( 22 | "Argument type not supported: {:#?}", 23 | &path.path.segments[0].arguments 24 | ), 25 | } 26 | } 27 | 28 | fn simple_type(name: String) -> Type { 29 | match name.as_str() { 30 | "String" => Type::String, 31 | "bool" => Type::Boolean, 32 | "u8" | "u16" | "u32" | "u64" => Type::UnsignedInt, 33 | "i8" | "i16" | "i32" | "i64" => Type::Int, 34 | "f8" | "f16" | "f32" | "f64" => Type::Float, 35 | _ => Type::Named(NamedType { 36 | name, 37 | generic_args: vec![], 38 | }), 39 | } 40 | } 41 | 42 | pub trait SynTypeBridge { 43 | fn syn_type(&self) -> &syn::Type; 44 | fn to_type(&self) -> Result { 45 | match self.syn_type() { 46 | SynType::Path(type_path) 47 | if type_path.qself.is_none() 48 | && type_path.path.leading_colon.is_none() 49 | && type_path.path.segments.len() == 1 => 50 | { 51 | let leading = leading_segment(type_path); 52 | let generics = generic_args(type_path)?; 53 | match leading.as_str() { 54 | "Option" if generics.len() == 1 => Ok(Type::Option(generics[0].clone().into())), 55 | "Box" if generics.len() == 1 => Ok(Type::Transparent(TransparentType { 56 | case: TransparentTypeCase::Box, 57 | type_: generics[0].clone().into(), 58 | })), 59 | "Rc" if generics.len() == 1 => Ok(Type::Transparent(TransparentType { 60 | case: TransparentTypeCase::Rc, 61 | type_: generics[0].clone().into(), 62 | })), 63 | "Arc" if generics.len() == 1 => Ok(Type::Transparent(TransparentType { 64 | case: TransparentTypeCase::Arc, 65 | type_: generics[0].clone().into(), 66 | })), 67 | "Vec" if generics.len() == 1 => Ok(Type::Array(generics[0].clone().into())), 68 | "HashMap" if generics.len() == 2 => Ok(Type::Map { 69 | key: generics[0].clone().into(), 70 | value: generics[1].clone().into(), 71 | }), 72 | "BTreeMap" if generics.len() == 2 => Ok(Type::Map { 73 | key: generics[0].clone().into(), 74 | value: generics[1].clone().into(), 75 | }), 76 | _ if generics.len() == 0 => Ok(simple_type(leading)), 77 | _ => syn_err!("Unsupported type type: {:#?}", &self.syn_type()), 78 | } 79 | } 80 | _ => syn_err!("Unsupported type: {:#?}", &self.syn_type()), 81 | } 82 | } 83 | } 84 | 85 | impl SynTypeBridge for syn::Type { 86 | fn syn_type(&self) -> &syn::Type { 87 | self 88 | } 89 | } 90 | 91 | fn get_struct_member(field: &Field) -> Result { 92 | // println!("Getting struct member from field: {:#?}", field); 93 | let name = match &field.ident { 94 | None => panic!("Struct fields must be named: {:#?}", field), 95 | Some(ident) => format!("{}", ident), 96 | }; 97 | 98 | let type_ = field.ty.to_type()?; 99 | 100 | Ok(NamedField { name, type_ }) 101 | } 102 | 103 | fn get_field_type(field: &Field) -> Result { 104 | // println!("Getting tuple member from field: {:#?}", field); 105 | match &field.ident { 106 | None => {} 107 | Some(_ident) => panic!("Tuple fields must not be named: {:#?}", field), 108 | }; 109 | 110 | let type_ = field.ty.to_type()?; 111 | 112 | Ok(type_) 113 | } 114 | 115 | pub trait FieldsBridge { 116 | fn fields(&self) -> &syn::Fields; 117 | fn to_fields(&self) -> Result { 118 | match &self.fields() { 119 | syn::Fields::Named(named) => Ok(TypeFieldsDefinition::Named(named.to_named_fields()?)), 120 | syn::Fields::Unnamed(unnamed) => { 121 | Ok(TypeFieldsDefinition::Tuple(unnamed.to_tuple_members()?)) 122 | } 123 | syn::Fields::Unit => Ok(TypeFieldsDefinition::Unit), 124 | } 125 | } 126 | } 127 | 128 | impl FieldsBridge for syn::Fields { 129 | fn fields(&self) -> &syn::Fields { 130 | self 131 | } 132 | } 133 | 134 | pub trait FieldsNamedBridge { 135 | fn fields_named(&self) -> &syn::FieldsNamed; 136 | fn to_named_fields(&self) -> Result> { 137 | (&self.fields_named().named) 138 | .into_iter() 139 | .map(|field: &Field| get_struct_member(&field)) 140 | .collect() 141 | } 142 | } 143 | 144 | impl FieldsNamedBridge for syn::FieldsNamed { 145 | fn fields_named(&self) -> &syn::FieldsNamed { 146 | self 147 | } 148 | } 149 | 150 | pub trait FieldsUnmnamedBridge { 151 | fn fields_unnamed(&self) -> &syn::FieldsUnnamed; 152 | fn to_tuple_members(&self) -> Result> { 153 | (&self.fields_unnamed().unnamed) 154 | .into_iter() 155 | .map(|field: &Field| get_field_type(&field)) 156 | .collect() 157 | } 158 | } 159 | 160 | impl FieldsUnmnamedBridge for syn::FieldsUnnamed { 161 | fn fields_unnamed(&self) -> &syn::FieldsUnnamed { 162 | self 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /type_reflect_macros/src/type_def/type_alias_def.rs: -------------------------------------------------------------------------------- 1 | use super::syn_type_utils::SynTypeBridge; 2 | use super::type_utils::TypeBridge; 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | use syn::{Ident, ItemType, Result}; 6 | use type_reflect_core::Type; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct TypeAliasDef { 10 | pub tokens: TokenStream, 11 | pub ident: Ident, 12 | source_type: Type, 13 | } 14 | 15 | impl TypeAliasDef { 16 | pub fn new(item: &ItemType) -> Result { 17 | Ok(Self { 18 | tokens: quote! { #item }, 19 | ident: item.ident.clone(), 20 | source_type: (item.ty).to_type()?, 21 | }) 22 | } 23 | 24 | pub fn emit(&self) -> TokenStream { 25 | let ident = &self.ident; 26 | let name_literal = format!("{}", ident); 27 | let rust = format!("{}", self.tokens); 28 | let type_ = self.source_type.emit_type(); 29 | 30 | quote! { 31 | impl Emittable for #ident { 32 | fn emit_with(emitter: &mut E) -> String { 33 | emitter.emit_alias::() 34 | } 35 | } 36 | 37 | impl AliasType for #ident { 38 | fn name() -> &'static str { 39 | #name_literal 40 | } 41 | fn source_type() -> Type { 42 | #type_ 43 | } 44 | fn rust() -> String { 45 | #rust.to_string() 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /type_reflect_macros/src/type_def/type_utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::*; 3 | use type_reflect_core::*; 4 | 5 | pub trait TypeBridge { 6 | fn type_(&self) -> &Type; 7 | fn emit_type(&self) -> TokenStream { 8 | match &self.type_() { 9 | Type::Named(name) => { 10 | let named_type = name.emit_named_type(); 11 | quote! { Type::Named(#named_type) } 12 | } 13 | Type::String => quote! { Type::String }, 14 | Type::Int => quote! { Type::Int }, 15 | Type::UnsignedInt => quote! { Type::UnsignedInt }, 16 | Type::Float => quote! { Type::Float }, 17 | Type::Boolean => quote! { Type::Boolean }, 18 | Type::Option(t) => { 19 | let inner = t.emit_type(); 20 | quote! { Type::Option( #inner.into() ) } 21 | } 22 | Type::Array(t) => { 23 | let inner = t.emit_type(); 24 | quote! { Type::Array( #inner.into() ) } 25 | } 26 | Type::Map { key, value } => { 27 | let key = key.emit_type(); 28 | let value = value.emit_type(); 29 | quote! { Type::Map{ key: #key.into(), value: #value.into() } } 30 | } 31 | Type::Transparent(t) => { 32 | let inner = t.emit_transparent_type(); 33 | quote! { Type::Transparent( #inner ) } 34 | } 35 | } 36 | } 37 | } 38 | 39 | impl TypeBridge for Type { 40 | fn type_(&self) -> &Type { 41 | self 42 | } 43 | } 44 | 45 | pub trait TypeFieldsDefinitionBridge { 46 | fn def(&self) -> &TypeFieldsDefinition; 47 | fn emit_def(&self) -> TokenStream { 48 | match &self.def() { 49 | TypeFieldsDefinition::Unit => quote! { TypeFieldsDefinition::Unit }, 50 | TypeFieldsDefinition::Tuple(inner) => { 51 | let mut types = quote! {}; 52 | 53 | for type_ in inner { 54 | let t = type_.emit_type(); 55 | types.extend(quote! {#t, }); 56 | } 57 | 58 | quote! { TypeFieldsDefinition::Tuple(vec![#types]) } 59 | } 60 | TypeFieldsDefinition::Named(inner) => { 61 | let mut mermbers = quote! {}; 62 | 63 | for member in inner { 64 | let m = member.emit_member(); 65 | mermbers.extend(quote! {#m, }); 66 | } 67 | 68 | quote! { TypeFieldsDefinition::Named(vec![#mermbers]) } 69 | } 70 | } 71 | } 72 | } 73 | 74 | impl TypeFieldsDefinitionBridge for TypeFieldsDefinition { 75 | fn def(&self) -> &TypeFieldsDefinition { 76 | self 77 | } 78 | } 79 | 80 | pub trait NamedFieldBridge { 81 | fn member(&self) -> &NamedField; 82 | fn emit_member(&self) -> TokenStream { 83 | let member = &self.member(); 84 | let name = &member.name; 85 | let type_ = member.type_.emit_type(); 86 | quote! { 87 | NamedField { 88 | name: #name.to_string(), 89 | type_: #type_, 90 | } 91 | } 92 | } 93 | } 94 | 95 | impl NamedFieldBridge for NamedField { 96 | fn member(&self) -> &NamedField { 97 | self 98 | } 99 | } 100 | 101 | pub trait NamedTypeBridge { 102 | fn named_type(&self) -> &NamedType; 103 | fn emit_named_type(&self) -> TokenStream { 104 | let name = &self.named_type().name; 105 | let generics: Vec = self 106 | .named_type() 107 | .generic_args 108 | .iter() 109 | .map(|arg| { 110 | let type_ = arg.emit_type(); 111 | quote! { 112 | Box<#type_> 113 | } 114 | }) 115 | .collect(); 116 | 117 | quote! { 118 | NamedType { 119 | name: #name.to_string(), 120 | generic_args: vec![#(#generics,)*], 121 | } 122 | } 123 | } 124 | } 125 | 126 | impl NamedTypeBridge for NamedType { 127 | fn named_type(&self) -> &NamedType { 128 | self 129 | } 130 | } 131 | 132 | pub trait TransparentTypeBridge { 133 | fn transparent_type(&self) -> &TransparentType; 134 | fn emit_transparent_type(&self) -> TokenStream { 135 | let case = match &self.transparent_type().case { 136 | TransparentTypeCase::Box => quote! { TransparentTypeCase::Box }, 137 | TransparentTypeCase::Rc => quote! { TransparentTypeCase::Rc }, 138 | TransparentTypeCase::Arc => quote! { TransparentTypeCase::Arc }, 139 | }; 140 | let inner = &self.transparent_type().type_.emit_type(); 141 | 142 | quote! { 143 | TransparentType { 144 | case: #case, 145 | type_: #inner.into(), 146 | } 147 | } 148 | } 149 | } 150 | 151 | impl TransparentTypeBridge for TransparentType { 152 | fn transparent_type(&self) -> &TransparentType { 153 | self 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /type_reflect_macros/src/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | 3 | /// Converts a rust identifier to a typescript identifier. 4 | #[allow(unused)] 5 | pub fn to_ts_ident(ident: &Ident) -> String { 6 | let ident = ident.to_string(); 7 | if ident.starts_with("r#") { 8 | ident.trim_start_matches("r#").to_owned() 9 | } else { 10 | ident 11 | } 12 | } 13 | 14 | /// Convert an arbitrary name to a valid Typescript field name. 15 | /// 16 | /// If the name contains special characters it will be wrapped in quotes. 17 | #[allow(unused)] 18 | pub fn raw_name_to_ts_field(value: String) -> String { 19 | let valid = value 20 | .chars() 21 | .all(|c| c.is_alphanumeric() || c == '_' || c == '$') 22 | && value 23 | .chars() 24 | .next() 25 | .map(|first| !first.is_numeric()) 26 | .unwrap_or(true); 27 | if !valid { 28 | format!(r#""{value}""#) 29 | } else { 30 | value 31 | } 32 | } 33 | 34 | // /// Parse all `#[ts(..)]` attributes from the given slice. 35 | // pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result> 36 | // where 37 | // A: TryFrom<&'a Attribute, Error = Error>, 38 | // { 39 | // Ok(attrs 40 | // .iter() 41 | // .filter(|a| a.path.is_ident("ts")) 42 | // .map(A::try_from) 43 | // .collect::>>()? 44 | // .into_iter()) 45 | // } 46 | 47 | // /// Parse all `#[serde(..)]` attributes from the given slice. 48 | // #[cfg(feature = "serde-compat")] 49 | // #[allow(unused)] 50 | // pub fn parse_serde_attrs<'a, A: TryFrom<&'a Attribute, Error = Error>>( 51 | // attrs: &'a [Attribute], 52 | // ) -> impl Iterator { 53 | // attrs 54 | // .iter() 55 | // .filter(|a| a.path.is_ident("serde")) 56 | // .flat_map(|attr| match A::try_from(attr) { 57 | // Ok(attr) => Some(attr), 58 | // Err(_) => { 59 | // use quote::ToTokens; 60 | // warning::print_warning( 61 | // "failed to parse serde attribute", 62 | // format!("{}", attr.to_token_stream()), 63 | // "ts-rs failed to parse this attribute. It will be ignored.", 64 | // ) 65 | // .unwrap(); 66 | // None 67 | // } 68 | // }) 69 | // .collect::>() 70 | // .into_iter() 71 | // } 72 | 73 | // #[cfg(feature = "serde-compat")] 74 | // mod warning { 75 | // use std::{fmt::Display, io::Write}; 76 | 77 | // use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; 78 | 79 | // // Sadly, it is impossible to raise a warning in a proc macro. 80 | // // This function prints a message which looks like a compiler warning. 81 | // pub fn print_warning( 82 | // title: impl Display, 83 | // content: impl Display, 84 | // note: impl Display, 85 | // ) -> std::io::Result<()> { 86 | // let make_color = |color: Color, bold: bool| { 87 | // let mut spec = ColorSpec::new(); 88 | // spec.set_fg(Some(color)).set_bold(bold).set_intense(true); 89 | // spec 90 | // }; 91 | 92 | // let yellow_bold = make_color(Color::Yellow, true); 93 | // let white_bold = make_color(Color::White, true); 94 | // let white = make_color(Color::White, false); 95 | // let blue = make_color(Color::Blue, true); 96 | 97 | // let writer = BufferWriter::stderr(ColorChoice::Auto); 98 | // let mut buffer = writer.buffer(); 99 | 100 | // buffer.set_color(&yellow_bold)?; 101 | // write!(&mut buffer, "warning")?; 102 | // buffer.set_color(&white_bold)?; 103 | // writeln!(&mut buffer, ": {}", title)?; 104 | 105 | // buffer.set_color(&blue)?; 106 | // writeln!(&mut buffer, " | ")?; 107 | 108 | // write!(&mut buffer, " | ")?; 109 | // buffer.set_color(&white)?; 110 | // writeln!(&mut buffer, "{}", content)?; 111 | 112 | // buffer.set_color(&blue)?; 113 | // writeln!(&mut buffer, " | ")?; 114 | 115 | // write!(&mut buffer, " = ")?; 116 | // buffer.set_color(&white_bold)?; 117 | // write!(&mut buffer, "note: ")?; 118 | // buffer.set_color(&white)?; 119 | // writeln!(&mut buffer, "{}", note)?; 120 | 121 | // writer.print(&buffer) 122 | // } 123 | // } 124 | --------------------------------------------------------------------------------