├── .editorconfig ├── .github └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── browser_test ├── example_output.js ├── example_output.ts ├── example_output_interfaces.ts └── test.html ├── example ├── example-models │ └── example_models.go └── example.go ├── go.mod ├── go.sum ├── makefile ├── scripts └── json_to_ts.sh ├── tscriptify └── main.go └── typescriptify ├── typescriptify.go ├── typescriptify_test.go └── utils.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = crlf 6 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | pull_request: 7 | branches: 'master' 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | go-version: [1.16.x] 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | name: Build and test 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | 20 | - name: Set up Go 1.x 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v2 28 | 29 | - name: Get dependencies 30 | run: | 31 | go mod download 32 | 33 | - name: Build 34 | run: | 35 | cd tscriptify 36 | go build -v . 37 | 38 | - name: Test 39 | run: | 40 | cd typescriptify 41 | go test -v . 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | *.iml 4 | tags 5 | tmp_* 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.10 4 | 5 | - missing prefix in struct map value types 6 | 7 | ## v0.1.8, v0.1.9 8 | 9 | - Typescript doc tags 10 | - Handle fields that is not annotated with json tag 11 | - Matrix testing 12 | 13 | ## v0.1.7 14 | 15 | - Handle packages with hyphens 16 | 17 | ## v0.1.6 18 | 19 | - Fix map keys if suffix/prefix specified 20 | - process customImports on Params with other flags 21 | 22 | ## v0.1.5 23 | 24 | - Fixed panic with arrays 25 | - Use go modules for dependency management 26 | - Example shell script how to create a typescript model directly from json 27 | 28 | ## v0.1.4 29 | 30 | - fix ignored pointers 31 | - interface cmdline flag 32 | 33 | ## v0.1.2 34 | 35 | - Log field and type creation to make the order (and why a type was converted) simpler to follow 36 | - Global custom types: Merge branch 'fix-33' of https://github.com/shackra/typescriptify-golang-structs into shackra-fix-33 37 | 38 | ## v0.1.1 39 | 40 | - custom types (insted of setting `ts_type` and `ts_transform` every time) 41 | 42 | ## v0.1.0 43 | 44 | - simplified conversion of objects 45 | - Pointer anonymous structs 46 | - more (and better) tests 47 | - maps of objects 48 | - convert in constructors (createFrom deprecated) 49 | - custom imports 50 | - Add `?` to field name if it's a pointer type 51 | - New way of defining enums 52 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [2015-] [Tomo Krajina] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Golang JSON to TypeScript model converter 2 | 3 | ## Installation 4 | 5 | The command-line tool: 6 | 7 | ``` 8 | go install github.com/tkrajina/typescriptify-golang-structs/tscriptify 9 | ``` 10 | 11 | The library: 12 | 13 | ``` 14 | go get github.com/tkrajina/typescriptify-golang-structs 15 | ``` 16 | 17 | ## Usage 18 | 19 | Use the command line tool: 20 | 21 | ``` 22 | tscriptify -package=package/with/your/models -target=target_ts_file.ts Model1 Model2 23 | ``` 24 | 25 | If you need to import a custom type in Typescript, you can pass the import string: 26 | 27 | ``` 28 | tscriptify -package=package/with/your/models -target=target_ts_file.ts -import="import { Decimal } from 'decimal.js'" Model1 Model2 29 | ``` 30 | 31 | If all your structs are in one file, you can convert them with: 32 | 33 | ``` 34 | tscriptify -package=package/with/your/models -target=target_ts_file.ts path/to/file/with/structs.go 35 | ``` 36 | 37 | Or by using it from your code: 38 | 39 | ```golang 40 | converter := typescriptify.New(). 41 | Add(Person{}). 42 | Add(Dummy{}) 43 | err := converter.ConvertToFile("ts/models.ts") 44 | if err != nil { 45 | panic(err.Error()) 46 | } 47 | ``` 48 | 49 | Command line options: 50 | 51 | ``` 52 | $ tscriptify --help 53 | Usage of tscriptify: 54 | -backup string 55 | Directory where backup files are saved 56 | -package string 57 | Path of the package with models 58 | -target string 59 | Target typescript file 60 | ``` 61 | 62 | ## Models and conversion 63 | 64 | If the `Person` structs contain a reference to the `Address` struct, then you don't have to add `Address` explicitly. Only fields with a valid `json` tag will be converted to TypeScript models. 65 | 66 | Example input structs: 67 | 68 | ```golang 69 | type Address struct { 70 | City string `json:"city"` 71 | Number float64 `json:"number"` 72 | Country string `json:"country,omitempty"` 73 | } 74 | 75 | type PersonalInfo struct { 76 | Hobbies []string `json:"hobby"` 77 | PetName string `json:"pet_name"` 78 | } 79 | 80 | type Person struct { 81 | Name string `json:"name"` 82 | PersonalInfo PersonalInfo `json:"personal_info"` 83 | Nicknames []string `json:"nicknames"` 84 | Addresses []Address `json:"addresses"` 85 | Address *Address `json:"address"` 86 | Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"` 87 | Friends []*Person `json:"friends"` 88 | } 89 | ``` 90 | 91 | Generated TypeScript: 92 | 93 | ```typescript 94 | export class Address { 95 | city: string; 96 | number: number; 97 | country?: string; 98 | 99 | constructor(source: any = {}) { 100 | if ('string' === typeof source) source = JSON.parse(source); 101 | this.city = source["city"]; 102 | this.number = source["number"]; 103 | this.country = source["country"]; 104 | } 105 | } 106 | export class PersonalInfo { 107 | hobby: string[]; 108 | pet_name: string; 109 | 110 | constructor(source: any = {}) { 111 | if ('string' === typeof source) source = JSON.parse(source); 112 | this.hobby = source["hobby"]; 113 | this.pet_name = source["pet_name"]; 114 | } 115 | } 116 | export class Person { 117 | name: string; 118 | personal_info: PersonalInfo; 119 | nicknames: string[]; 120 | addresses: Address[]; 121 | address?: Address; 122 | metadata: {[key:string]:string}; 123 | friends: Person[]; 124 | 125 | constructor(source: any = {}) { 126 | if ('string' === typeof source) source = JSON.parse(source); 127 | this.name = source["name"]; 128 | this.personal_info = this.convertValues(source["personal_info"], PersonalInfo); 129 | this.nicknames = source["nicknames"]; 130 | this.addresses = this.convertValues(source["addresses"], Address); 131 | this.address = this.convertValues(source["address"], Address); 132 | this.metadata = source["metadata"]; 133 | this.friends = this.convertValues(source["friends"], Person); 134 | } 135 | 136 | convertValues(a: any, classs: any, asMap: boolean = false): any { 137 | if (!a) { 138 | return a; 139 | } 140 | if (a.slice) { 141 | return (a as any[]).map(elem => this.convertValues(elem, classs)); 142 | } else if ("object" === typeof a) { 143 | if (asMap) { 144 | for (const key of Object.keys(a)) { 145 | a[key] = new classs(a[key]); 146 | } 147 | return a; 148 | } 149 | return new classs(a); 150 | } 151 | return a; 152 | } 153 | } 154 | ``` 155 | 156 | If you prefer interfaces, the output is: 157 | 158 | ```typescript 159 | export interface Address { 160 | city: string; 161 | number: number; 162 | country?: string; 163 | } 164 | export interface PersonalInfo { 165 | hobby: string[]; 166 | pet_name: string; 167 | } 168 | export interface Person { 169 | name: string; 170 | personal_info: PersonalInfo; 171 | nicknames: string[]; 172 | addresses: Address[]; 173 | address?: Address; 174 | metadata: {[key:string]:string}; 175 | friends: Person[]; 176 | } 177 | ``` 178 | 179 | In TypeScript you can just cast your json object in any of those models: 180 | 181 | ```typescript 182 | var person = {"name":"Me myself","nicknames":["aaa", "bbb"]}; 183 | console.log(person.name); 184 | // The TypeScript compiler will throw an error for this line 185 | console.log(person.something); 186 | ``` 187 | 188 | ## Custom Typescript code 189 | 190 | Any custom code can be added to Typescript models: 191 | 192 | ```typescript 193 | class Address { 194 | street : string; 195 | no : number; 196 | //[Address:] 197 | country: string; 198 | getStreetAndNumber() { 199 | return street + " " + number; 200 | } 201 | //[end] 202 | } 203 | ``` 204 | 205 | The lines between `//[Address:]` and `//[end]` will be left intact after `ConvertToFile()`. 206 | 207 | If your custom code contain methods, then just casting yout object to the target class (with ` {...}`) won't work because the casted object won't contain your methods. 208 | 209 | In that case use the constructor: 210 | 211 | ```typescript 212 | var person = new Person({"name":"Me myself","nicknames":["aaa", "bbb"]}); 213 | ``` 214 | 215 | If you use golang JSON structs as responses from your API, you may want to have a common prefix for all the generated models: 216 | 217 | ```golang 218 | converter := typescriptify.New(). 219 | converter.Prefix = "API_" 220 | converter.Add(Person{}) 221 | ``` 222 | 223 | The model name will be `API_Person` instead of `Person`. 224 | 225 | ## Field comments 226 | 227 | Field documentation comments can be added with the `ts_doc` tag: 228 | 229 | ```golang 230 | type Person struct { 231 | Name string `json:"name" ts_doc:"This is a comment"` 232 | } 233 | ``` 234 | 235 | Generated typescript: 236 | 237 | ```typescript 238 | export class Person { 239 | /** This is a comment */ 240 | name: string; 241 | } 242 | ``` 243 | 244 | ## Custom types 245 | 246 | If your field has a type not supported by typescriptify which can be JSONized as is, then you can use the `ts_type` tag to specify the typescript type to use: 247 | 248 | ```golang 249 | type Data struct { 250 | Counters map[string]int `json:"counters" ts_type:"CustomType"` 251 | } 252 | ``` 253 | 254 | ...will create: 255 | 256 | ```typescript 257 | export class Data { 258 | counters: CustomType; 259 | } 260 | ``` 261 | 262 | If the JSON field needs some special handling before converting it to a javascript object, use `ts_transform`. 263 | For example: 264 | 265 | ```golang 266 | type Data struct { 267 | Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"` 268 | } 269 | ``` 270 | 271 | Generated typescript: 272 | 273 | ```typescript 274 | export class Date { 275 | time: Date; 276 | 277 | constructor(source: any = {}) { 278 | if ('string' === typeof source) source = JSON.parse(source); 279 | this.time = new Date(source["time"]); 280 | } 281 | } 282 | ``` 283 | 284 | In this case, you should always use `new Data(json)` instead of just casting `json`. 285 | 286 | If you use a custom type that has to be imported, you can do the following: 287 | 288 | ```golang 289 | converter := typescriptify.New() 290 | converter.AddImport("import Decimal from 'decimal.js'") 291 | ``` 292 | 293 | This will put your import on top of the generated file. 294 | 295 | ## Global custom types 296 | 297 | Additionally, you can tell the library to automatically use a given Typescript type and custom transformation for a type: 298 | 299 | ```golang 300 | converter := New() 301 | converter.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"}) 302 | ``` 303 | 304 | If you only want to change `ts_transform` but not `ts_type`, you can pass an empty string. 305 | 306 | ## Enums 307 | 308 | There are two ways to create enums. 309 | 310 | ### Enums with TSName() 311 | 312 | In this case you must provide a list of enum values and the enum type must have a `TSName() string` method 313 | 314 | ```golang 315 | type Weekday int 316 | 317 | const ( 318 | Sunday Weekday = iota 319 | Monday 320 | Tuesday 321 | Wednesday 322 | Thursday 323 | Friday 324 | Saturday 325 | ) 326 | 327 | var AllWeekdays = []Weekday{ Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, } 328 | 329 | func (w Weekday) TSName() string { 330 | switch w { 331 | case Sunday: 332 | return "SUNDAY" 333 | case Monday: 334 | return "MONDAY" 335 | case Tuesday: 336 | return "TUESDAY" 337 | case Wednesday: 338 | return "WEDNESDAY" 339 | case Thursday: 340 | return "THURSDAY" 341 | case Friday: 342 | return "FRIDAY" 343 | case Saturday: 344 | return "SATURDAY" 345 | default: 346 | return "???" 347 | } 348 | } 349 | ``` 350 | 351 | If this is too verbose for you, you can also provide a list of enums and enum names: 352 | 353 | ```golang 354 | var AllWeekdays = []struct { 355 | Value Weekday 356 | TSName string 357 | }{ 358 | {Sunday, "SUNDAY"}, 359 | {Monday, "MONDAY"}, 360 | {Tuesday, "TUESDAY"}, 361 | {Wednesday, "WEDNESDAY"}, 362 | {Thursday, "THURSDAY"}, 363 | {Friday, "FRIDAY"}, 364 | {Saturday, "SATURDAY"}, 365 | } 366 | ``` 367 | 368 | Then, when converting models `AddEnum()` to specify the enum: 369 | 370 | ```golang 371 | converter := New(). 372 | AddEnum(AllWeekdays) 373 | ``` 374 | 375 | The resulting code will be: 376 | 377 | ```typescript 378 | export enum Weekday { 379 | SUNDAY = 0, 380 | MONDAY = 1, 381 | TUESDAY = 2, 382 | WEDNESDAY = 3, 383 | THURSDAY = 4, 384 | FRIDAY = 5, 385 | SATURDAY = 6, 386 | } 387 | export class Holliday { 388 | name: string; 389 | weekday: Weekday; 390 | } 391 | ``` 392 | 393 | ## License 394 | 395 | This library is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 396 | 397 | -------------------------------------------------------------------------------- /browser_test/example_output.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Do not change, this code is generated from Golang structs */ 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.Person = exports.PersonalInfo = exports.Address = void 0; 5 | var Address = /** @class */ (function () { 6 | function Address(source) { 7 | if (source === void 0) { source = {}; } 8 | var _this = this; 9 | //[Address:] 10 | /* Custom code here */ 11 | this.getAddressString = function () { 12 | return _this.city + " " + _this.number; 13 | }; 14 | if ('string' === typeof source) 15 | source = JSON.parse(source); 16 | this.city = source["city"]; 17 | this.number = source["number"]; 18 | this.country = source["country"]; 19 | } 20 | return Address; 21 | }()); 22 | exports.Address = Address; 23 | var PersonalInfo = /** @class */ (function () { 24 | function PersonalInfo(source) { 25 | if (source === void 0) { source = {}; } 26 | var _this = this; 27 | //[PersonalInfo:] 28 | this.getPersonalInfoString = function () { 29 | return "pet:" + _this.pet_name; 30 | }; 31 | if ('string' === typeof source) 32 | source = JSON.parse(source); 33 | this.hobby = source["hobby"]; 34 | this.pet_name = source["pet_name"]; 35 | } 36 | return PersonalInfo; 37 | }()); 38 | exports.PersonalInfo = PersonalInfo; 39 | var Person = /** @class */ (function () { 40 | function Person(source) { 41 | if (source === void 0) { source = {}; } 42 | var _this = this; 43 | //[Person:] 44 | this.getInfo = function () { 45 | return "name:" + _this.name; 46 | }; 47 | if ('string' === typeof source) 48 | source = JSON.parse(source); 49 | this.name = source["name"]; 50 | this.personal_info = this.convertValues(source["personal_info"], PersonalInfo); 51 | this.nicknames = source["nicknames"]; 52 | this.addresses = this.convertValues(source["addresses"], Address); 53 | this.address = this.convertValues(source["address"], Address); 54 | this.metadata = source["metadata"]; 55 | this.friends = this.convertValues(source["friends"], Person); 56 | } 57 | Person.prototype.convertValues = function (a, classs, asMap) { 58 | var _this = this; 59 | if (asMap === void 0) { asMap = false; } 60 | if (!a) { 61 | return a; 62 | } 63 | if (Array.isArray(a)) { 64 | return a.map(function (elem) { return _this.convertValues(elem, classs); }); 65 | } 66 | else if ("object" === typeof a) { 67 | if (asMap) { 68 | for (var _i = 0, _a = Object.keys(a); _i < _a.length; _i++) { 69 | var key = _a[_i]; 70 | a[key] = new classs(a[key]); 71 | } 72 | return a; 73 | } 74 | return new classs(a); 75 | } 76 | return a; 77 | }; 78 | return Person; 79 | }()); 80 | exports.Person = Person; 81 | -------------------------------------------------------------------------------- /browser_test/example_output.ts: -------------------------------------------------------------------------------- 1 | /* Do not change, this code is generated from Golang structs */ 2 | 3 | 4 | export class Address { 5 | city: string; 6 | number: number; 7 | country?: string; 8 | 9 | constructor(source: any = {}) { 10 | if ('string' === typeof source) source = JSON.parse(source); 11 | this.city = source["city"]; 12 | this.number = source["number"]; 13 | this.country = source["country"]; 14 | } 15 | //[Address:] 16 | /* Custom code here */ 17 | 18 | getAddressString = () => { 19 | return this.city + " " + this.number; 20 | } 21 | 22 | //[end] 23 | } 24 | export class PersonalInfo { 25 | hobby: string[]; 26 | pet_name: string; 27 | 28 | constructor(source: any = {}) { 29 | if ('string' === typeof source) source = JSON.parse(source); 30 | this.hobby = source["hobby"]; 31 | this.pet_name = source["pet_name"]; 32 | } 33 | //[PersonalInfo:] 34 | 35 | getPersonalInfoString = () => { 36 | return "pet:" + this.pet_name; 37 | } 38 | 39 | //[end] 40 | } 41 | export class Person { 42 | name: string; 43 | personal_info: PersonalInfo; 44 | nicknames: string[]; 45 | addresses: Address[]; 46 | address?: Address; 47 | metadata: {[key:string]:string}; 48 | friends: Person[]; 49 | 50 | constructor(source: any = {}) { 51 | if ('string' === typeof source) source = JSON.parse(source); 52 | this.name = source["name"]; 53 | this.personal_info = this.convertValues(source["personal_info"], PersonalInfo); 54 | this.nicknames = source["nicknames"]; 55 | this.addresses = this.convertValues(source["addresses"], Address); 56 | this.address = this.convertValues(source["address"], Address); 57 | this.metadata = source["metadata"]; 58 | this.friends = this.convertValues(source["friends"], Person); 59 | } 60 | 61 | convertValues(a: any, classs: any, asMap: boolean = false): any { 62 | if (!a) { 63 | return a; 64 | } 65 | if (Array.isArray(a)) { 66 | return (a as any[]).map(elem => this.convertValues(elem, classs)); 67 | } else if ("object" === typeof a) { 68 | if (asMap) { 69 | for (const key of Object.keys(a)) { 70 | a[key] = new classs(a[key]); 71 | } 72 | return a; 73 | } 74 | return new classs(a); 75 | } 76 | return a; 77 | } 78 | //[Person:] 79 | 80 | getInfo = () => { 81 | return "name:" + this.name; 82 | } 83 | 84 | //[end] 85 | } -------------------------------------------------------------------------------- /browser_test/example_output_interfaces.ts: -------------------------------------------------------------------------------- 1 | /* Do not change, this code is generated from Golang structs */ 2 | 3 | 4 | export interface Address { 5 | city: string; 6 | number: number; 7 | country?: string; 8 | } 9 | export interface PersonalInfo { 10 | hobby: string[]; 11 | pet_name: string; 12 | } 13 | export interface Person { 14 | name: string; 15 | personal_info: PersonalInfo; 16 | nicknames: string[]; 17 | addresses: Address[]; 18 | address?: Address; 19 | metadata: {[key:string]:string}; 20 | friends: Person[]; 21 | //[Person:] 22 | /* Custom code here */ 23 | 24 | [key: string]: any 25 | 26 | //[end] 27 | } -------------------------------------------------------------------------------- /browser_test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test 4 | 5 | 57 | 58 | 59 |

Test

60 |

OK (check browser console for errors)?

61 | 62 | 63 | -------------------------------------------------------------------------------- /example/example-models/example_models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Address struct { 4 | // Used in html 5 | City string `json:"city"` 6 | Number float64 `json:"number"` 7 | Country string `json:"country,omitempty"` 8 | } 9 | 10 | type PersonalInfo struct { 11 | Hobbies []string `json:"hobby"` 12 | PetName string `json:"pet_name"` 13 | } 14 | 15 | type Person struct { 16 | Name string `json:"name"` 17 | PersonalInfo PersonalInfo `json:"personal_info"` 18 | Nicknames []string `json:"nicknames"` 19 | Addresses []Address `json:"addresses"` 20 | Address *Address `json:"address"` 21 | Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"` 22 | Friends []*Person `json:"friends"` 23 | } 24 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/tkrajina/typescriptify-golang-structs/typescriptify" 4 | 5 | type Address struct { 6 | // Used in html 7 | City string `json:"city"` 8 | Number float64 `json:"number"` 9 | Country string `json:"country,omitempty"` 10 | } 11 | 12 | type PersonalInfo struct { 13 | Hobbies []string `json:"hobby"` 14 | PetName string `json:"pet_name"` 15 | } 16 | 17 | type Person struct { 18 | Name string `json:"name"` 19 | PersonalInfo PersonalInfo `json:"personal_info"` 20 | Nicknames []string `json:"nicknames"` 21 | Addresses []Address `json:"addresses"` 22 | Address *Address `json:"address"` 23 | Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"` 24 | Friends []*Person `json:"friends"` 25 | } 26 | 27 | func main() { 28 | converter := typescriptify.New() 29 | converter.CreateConstructor = true 30 | converter.Indent = " " 31 | converter.BackupDir = "" 32 | 33 | converter.Add(Person{}) 34 | 35 | err := converter.ConvertToFile("browser_test/example_output.ts") 36 | if err != nil { 37 | panic(err.Error()) 38 | } 39 | 40 | converter.CreateInterface = true 41 | err = converter.ConvertToFile("browser_test/example_output_interfaces.ts") 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tkrajina/typescriptify-golang-structs 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/stretchr/testify v1.7.0 7 | github.com/tkrajina/go-reflector v0.5.5 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | github.com/tkrajina/go-reflector v0.5.4 h1:dS9aJEa/eYNQU/fwsb5CSiATOxcNyA/gG/A7a582D5s= 9 | github.com/tkrajina/go-reflector v0.5.4/go.mod h1:9PyLgEOzc78ey/JmQQHbW8cQJ1oucLlNQsg8yFvkVk8= 10 | github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= 11 | github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | go build -i -v -o /dev/null ./... 4 | 5 | .PHONY: install 6 | install: 7 | go install ./... 8 | 9 | .PHONY: test 10 | test: lint 11 | go test ./... 12 | go run example/example.go 13 | tsc browser_test/example_output.ts 14 | # Make sure dommandline tool works: 15 | go run tscriptify/main.go -package github.com/tkrajina/typescriptify-golang-structs/example/example-models -verbose -target tmp_classes.ts example/example-models/example_models.go 16 | go run tscriptify/main.go -package github.com/tkrajina/typescriptify-golang-structs/example/example-models -verbose -target tmp_interfaces.ts -interface example/example-models/example_models.go 17 | 18 | .PHONY: lint 19 | lint: 20 | go vet ./... 21 | -golangci-lint run 22 | -------------------------------------------------------------------------------- /scripts/json_to_ts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | json=$1 5 | model_name=$2 6 | 7 | if [ -z "$json" ]; 8 | then 9 | echo "No JSON specified" 10 | exit 1 11 | fi 12 | if [ -z "$model_name" ]; 13 | then 14 | echo "No model name specified" 15 | exit 1 16 | fi 17 | 18 | mkdir -p $GOPATH/src/tmp_models 19 | echo "Using $GOPATH/src/tmp_models ad temporary models package" 20 | cat $model_name.json | gojson -pkg tmp_models -name=$model_name > $GOPATH/src/tmp_models/$model_name.go 21 | tscriptify -package=tmp_models -target $model_name.ts $model_name 22 | echo "Saved to $model_name" 23 | -------------------------------------------------------------------------------- /tscriptify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "text/template" 13 | ) 14 | 15 | type arrayImports []string 16 | 17 | func (i *arrayImports) String() string { 18 | return "// custom imports:\n\n" + strings.Join(*i, "\n") 19 | } 20 | 21 | func (i *arrayImports) Set(value string) error { 22 | *i = append(*i, value) 23 | return nil 24 | } 25 | 26 | const TEMPLATE = `package main 27 | 28 | import ( 29 | "fmt" 30 | 31 | m "{{ .ModelsPackage }}" 32 | "github.com/tkrajina/typescriptify-golang-structs/typescriptify" 33 | ) 34 | 35 | func main() { 36 | t := typescriptify.New() 37 | t.CreateInterface = {{ .Interface }} 38 | {{ range $key, $value := .InitParams }} t.{{ $key }}={{ $value }} 39 | {{ end }} 40 | {{ range .Structs }} t.Add({{ . }}{}) 41 | {{ end }} 42 | {{ range .CustomImports }} t.AddImport("{{ . }}") 43 | {{ end }} 44 | err := t.ConvertToFile("{{ .TargetFile }}") 45 | if err != nil { 46 | panic(err.Error()) 47 | } 48 | fmt.Println("OK") 49 | }` 50 | 51 | type Params struct { 52 | ModelsPackage string 53 | TargetFile string 54 | Structs []string 55 | InitParams map[string]interface{} 56 | CustomImports arrayImports 57 | Interface bool 58 | Verbose bool 59 | } 60 | 61 | func main() { 62 | var p Params 63 | var backupDir string 64 | flag.StringVar(&p.ModelsPackage, "package", "", "Path of the package with models") 65 | flag.StringVar(&p.TargetFile, "target", "", "Target typescript file") 66 | flag.StringVar(&backupDir, "backup", "", "Directory where backup files are saved") 67 | flag.BoolVar(&p.Interface, "interface", false, "Create interfaces (not classes)") 68 | flag.Var(&p.CustomImports, "import", "Typescript import for your custom type, repeat this option for each import needed") 69 | flag.BoolVar(&p.Verbose, "verbose", false, "Verbose logs") 70 | flag.Parse() 71 | 72 | structs := []string{} 73 | for _, structOrGoFile := range flag.Args() { 74 | if strings.HasSuffix(structOrGoFile, ".go") { 75 | fmt.Println("Parsing:", structOrGoFile) 76 | fileStructs, err := GetGolangFileStructs(structOrGoFile) 77 | if err != nil { 78 | panic(fmt.Sprintf("Error loading/parsing golang file %s: %s", structOrGoFile, err.Error())) 79 | } 80 | structs = append(structs, fileStructs...) 81 | } else { 82 | structs = append(structs, structOrGoFile) 83 | } 84 | } 85 | 86 | if len(p.ModelsPackage) == 0 { 87 | fmt.Fprintln(os.Stderr, "No package given") 88 | os.Exit(1) 89 | } 90 | if len(p.TargetFile) == 0 { 91 | fmt.Fprintln(os.Stderr, "No target file") 92 | os.Exit(1) 93 | } 94 | 95 | t := template.Must(template.New("").Parse(TEMPLATE)) 96 | 97 | f, err := os.CreateTemp(os.TempDir(), "typescriptify_*.go") 98 | handleErr(err) 99 | defer f.Close() 100 | 101 | structsArr := make([]string, 0) 102 | for _, str := range structs { 103 | str = strings.TrimSpace(str) 104 | if len(str) > 0 { 105 | structsArr = append(structsArr, "m."+str) 106 | } 107 | } 108 | 109 | p.Structs = structsArr 110 | p.InitParams = map[string]interface{}{ 111 | "BackupDir": fmt.Sprintf(`"%s"`, backupDir), 112 | } 113 | err = t.Execute(f, p) 114 | handleErr(err) 115 | 116 | if p.Verbose { 117 | byts, err := os.ReadFile(f.Name()) 118 | handleErr(err) 119 | fmt.Printf("\nCompiling generated code (%s):\n%s\n----------------------------------------------------------------------------------------------------\n", f.Name(), string(byts)) 120 | } 121 | 122 | cmd := exec.Command("go", "run", f.Name()) 123 | fmt.Println(strings.Join(cmd.Args, " ")) 124 | output, err := cmd.CombinedOutput() 125 | if err != nil { 126 | fmt.Println(string(output)) 127 | handleErr(err) 128 | } 129 | fmt.Println(string(output)) 130 | } 131 | 132 | func GetGolangFileStructs(filename string) ([]string, error) { 133 | fset := token.NewFileSet() // positions are relative to fset 134 | 135 | f, err := parser.ParseFile(fset, filename, nil, 0) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | v := &AVisitor{} 141 | ast.Walk(v, f) 142 | 143 | return v.structs, nil 144 | } 145 | 146 | type AVisitor struct { 147 | structNameCandidate string 148 | structs []string 149 | } 150 | 151 | func (v *AVisitor) Visit(node ast.Node) ast.Visitor { 152 | if node != nil { 153 | switch t := node.(type) { 154 | case *ast.Ident: 155 | v.structNameCandidate = t.Name 156 | case *ast.StructType: 157 | if len(v.structNameCandidate) > 0 { 158 | v.structs = append(v.structs, v.structNameCandidate) 159 | v.structNameCandidate = "" 160 | } 161 | default: 162 | v.structNameCandidate = "" 163 | } 164 | } 165 | return v 166 | } 167 | 168 | func handleErr(err error) { 169 | if err != nil { 170 | panic(err.Error()) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /typescriptify/typescriptify.go: -------------------------------------------------------------------------------- 1 | package typescriptify 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path" 8 | "reflect" 9 | "strings" 10 | "time" 11 | 12 | "github.com/tkrajina/go-reflector/reflector" 13 | ) 14 | 15 | const ( 16 | tsDocTag = "ts_doc" 17 | tsTransformTag = "ts_transform" 18 | tsType = "ts_type" 19 | jsonTag = "json" 20 | tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any { 21 | if (!a) { 22 | return a; 23 | } 24 | if (Array.isArray(a)) { 25 | return (a as any[]).map(elem => this.convertValues(elem, classs)); 26 | } else if ("object" === typeof a) { 27 | if (asMap) { 28 | for (const key of Object.keys(a)) { 29 | a[key] = new classs(a[key]); 30 | } 31 | return a; 32 | } 33 | return new classs(a); 34 | } 35 | return a; 36 | }` 37 | ) 38 | 39 | // TypeOptions overrides options set by `ts_*` tags. 40 | type TypeOptions struct { 41 | TSType string 42 | TSDoc string 43 | TSTransform string 44 | } 45 | 46 | // StructType stores settings for transforming one Golang struct. 47 | type StructType struct { 48 | Type reflect.Type 49 | FieldOptions map[reflect.Type]TypeOptions 50 | } 51 | 52 | func NewStruct(i interface{}) *StructType { 53 | return &StructType{ 54 | Type: reflect.TypeOf(i), 55 | } 56 | } 57 | 58 | func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType { 59 | if st.FieldOptions == nil { 60 | st.FieldOptions = map[reflect.Type]TypeOptions{} 61 | } 62 | var typ reflect.Type 63 | if ty, is := i.(reflect.Type); is { 64 | typ = ty 65 | } else { 66 | typ = reflect.TypeOf(i) 67 | } 68 | st.FieldOptions[typ] = opts 69 | return st 70 | } 71 | 72 | type EnumType struct { 73 | Type reflect.Type 74 | } 75 | 76 | type enumElement struct { 77 | value interface{} 78 | name string 79 | } 80 | 81 | type TypeScriptify struct { 82 | Prefix string 83 | Suffix string 84 | Indent string 85 | CreateFromMethod bool 86 | CreateConstructor bool 87 | BackupDir string // If empty no backup 88 | DontExport bool 89 | CreateInterface bool 90 | CustomJsonTag string 91 | customImports []string 92 | customCodeBefore []string 93 | customCodeAfter []string 94 | silent bool 95 | 96 | structTypes []StructType 97 | enumTypes []EnumType 98 | enums map[reflect.Type][]enumElement 99 | kinds map[reflect.Kind]string 100 | 101 | fieldTypeOptions map[reflect.Type]TypeOptions 102 | 103 | // throwaway, used when converting 104 | alreadyConverted map[reflect.Type]bool 105 | } 106 | 107 | func New() *TypeScriptify { 108 | result := new(TypeScriptify) 109 | result.Indent = "\t" 110 | result.BackupDir = "." 111 | 112 | kinds := make(map[reflect.Kind]string) 113 | 114 | kinds[reflect.Bool] = "boolean" 115 | kinds[reflect.Interface] = "any" 116 | 117 | kinds[reflect.Int] = "number" 118 | kinds[reflect.Int8] = "number" 119 | kinds[reflect.Int16] = "number" 120 | kinds[reflect.Int32] = "number" 121 | kinds[reflect.Int64] = "number" 122 | kinds[reflect.Uint] = "number" 123 | kinds[reflect.Uint8] = "number" 124 | kinds[reflect.Uint16] = "number" 125 | kinds[reflect.Uint32] = "number" 126 | kinds[reflect.Uint64] = "number" 127 | kinds[reflect.Float32] = "number" 128 | kinds[reflect.Float64] = "number" 129 | 130 | kinds[reflect.String] = "string" 131 | 132 | result.kinds = kinds 133 | 134 | result.Indent = " " 135 | result.CreateFromMethod = false 136 | result.CreateConstructor = true 137 | 138 | return result 139 | } 140 | 141 | func deepFields(typeOf reflect.Type) []reflect.StructField { 142 | fields := make([]reflect.StructField, 0) 143 | 144 | if typeOf.Kind() == reflect.Ptr { 145 | typeOf = typeOf.Elem() 146 | } 147 | 148 | if typeOf.Kind() != reflect.Struct { 149 | return fields 150 | } 151 | 152 | for i := 0; i < typeOf.NumField(); i++ { 153 | f := typeOf.Field(i) 154 | 155 | kind := f.Type.Kind() 156 | if f.Anonymous && kind == reflect.Struct { 157 | //fmt.Println(v.Interface()) 158 | fields = append(fields, deepFields(f.Type)...) 159 | } else if f.Anonymous && kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct { 160 | //fmt.Println(v.Interface()) 161 | fields = append(fields, deepFields(f.Type.Elem())...) 162 | } else { 163 | fields = append(fields, f) 164 | } 165 | } 166 | 167 | return fields 168 | } 169 | 170 | func (ts *TypeScriptify) Silent() *TypeScriptify { 171 | ts.silent = true 172 | return ts 173 | } 174 | 175 | func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) { 176 | if ts.silent { 177 | return 178 | } 179 | 180 | fmt.Printf(strings.Repeat(" ", depth)+s+"\n", args...) 181 | } 182 | 183 | // ManageType can define custom options for fields of a specified type. 184 | // 185 | // This can be used instead of setting ts_type and ts_transform for all fields of a certain type. 186 | func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify { 187 | var typ reflect.Type 188 | switch t := fld.(type) { 189 | case reflect.Type: 190 | typ = t 191 | default: 192 | typ = reflect.TypeOf(fld) 193 | } 194 | if t.fieldTypeOptions == nil { 195 | t.fieldTypeOptions = map[reflect.Type]TypeOptions{} 196 | } 197 | t.fieldTypeOptions[typ] = opts 198 | return t 199 | } 200 | 201 | func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify { 202 | t.CreateFromMethod = b 203 | return t 204 | } 205 | 206 | func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify { 207 | t.CreateInterface = b 208 | return t 209 | } 210 | 211 | func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify { 212 | t.CreateConstructor = b 213 | return t 214 | } 215 | 216 | func (t *TypeScriptify) WithIndent(i string) *TypeScriptify { 217 | t.Indent = i 218 | return t 219 | } 220 | 221 | func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify { 222 | t.BackupDir = b 223 | return t 224 | } 225 | 226 | func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify { 227 | t.Prefix = p 228 | return t 229 | } 230 | 231 | func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify { 232 | t.Suffix = s 233 | return t 234 | } 235 | 236 | func (t *TypeScriptify) WithCustomJsonTag(tag string) *TypeScriptify { 237 | t.CustomJsonTag = tag 238 | return t 239 | } 240 | 241 | func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify { 242 | switch ty := obj.(type) { 243 | case StructType: 244 | t.structTypes = append(t.structTypes, ty) 245 | case *StructType: 246 | t.structTypes = append(t.structTypes, *ty) 247 | case reflect.Type: 248 | t.AddType(ty) 249 | default: 250 | t.AddType(reflect.TypeOf(obj)) 251 | } 252 | return t 253 | } 254 | 255 | func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify { 256 | t.structTypes = append(t.structTypes, StructType{Type: typeOf}) 257 | return t 258 | } 259 | 260 | func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) { 261 | keyType := field.Type.Key() 262 | valueType := field.Type.Elem() 263 | valueTypeName := valueType.Name() 264 | if name, ok := t.types[valueType.Kind()]; ok { 265 | valueTypeName = name 266 | } 267 | if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice { 268 | valueTypeName = valueType.Elem().Name() + "[]" 269 | } 270 | if valueType.Kind() == reflect.Ptr { 271 | valueTypeName = valueType.Elem().Name() 272 | } 273 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "") 274 | 275 | keyTypeStr := keyType.Name() 276 | // Key should always be string, no need for this: 277 | // _, isSimple := t.types[keyType.Kind()] 278 | // if !isSimple { 279 | // keyTypeStr = t.prefix + keyType.Name() + t.suffix 280 | // } 281 | 282 | if valueType.Kind() == reflect.Struct { 283 | t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, t.prefix+valueTypeName)) 284 | t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = this.convertValues(source[\"%s\"], %s, true);", t.indent, t.indent, strippedFieldName, strippedFieldName, t.prefix+valueTypeName+t.suffix)) 285 | } else { 286 | t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, valueTypeName)) 287 | t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = source[\"%s\"];", t.indent, t.indent, strippedFieldName, strippedFieldName)) 288 | } 289 | } 290 | 291 | func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify { 292 | if t.enums == nil { 293 | t.enums = map[reflect.Type][]enumElement{} 294 | } 295 | items := reflect.ValueOf(values) 296 | if items.Kind() != reflect.Slice { 297 | panic(fmt.Sprintf("Values for %T isn't a slice", values)) 298 | } 299 | 300 | var elements []enumElement 301 | for i := 0; i < items.Len(); i++ { 302 | item := items.Index(i) 303 | 304 | var el enumElement 305 | if item.Kind() == reflect.Struct { 306 | r := reflector.New(item.Interface()) 307 | val, err := r.Field("Value").Get() 308 | if err != nil { 309 | panic(fmt.Sprint("missing Type field in ", item.Type().String())) 310 | } 311 | name, err := r.Field("TSName").Get() 312 | if err != nil { 313 | panic(fmt.Sprint("missing TSName field in ", item.Type().String())) 314 | } 315 | el.value = val 316 | el.name = name.(string) 317 | } else { 318 | el.value = item.Interface() 319 | if tsNamer, is := item.Interface().(TSNamer); is { 320 | el.name = tsNamer.TSName() 321 | } else { 322 | panic(fmt.Sprint(item.Type().String(), " has no TSName method")) 323 | } 324 | } 325 | 326 | elements = append(elements, el) 327 | } 328 | ty := reflect.TypeOf(elements[0].value) 329 | t.enums[ty] = elements 330 | t.enumTypes = append(t.enumTypes, EnumType{Type: ty}) 331 | 332 | return t 333 | } 334 | 335 | // AddEnumValues is deprecated, use `AddEnum()` 336 | func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify { 337 | t.AddEnum(values) 338 | return t 339 | } 340 | 341 | func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) { 342 | if t.CreateFromMethod { 343 | fmt.Fprintln(os.Stderr, "FromMethod METHOD IS DEPRECATED AND WILL BE REMOVED!!!!!!") 344 | } 345 | 346 | t.alreadyConverted = make(map[reflect.Type]bool) 347 | depth := 0 348 | 349 | result := "" 350 | if len(t.customImports) > 0 { 351 | // Put the custom imports, i.e.: `import Decimal from 'decimal.js'` 352 | for _, cimport := range t.customImports { 353 | result += cimport + "\n" 354 | } 355 | } 356 | 357 | if len(t.customCodeBefore) > 0 { 358 | result += "\n" 359 | for _, code := range t.customCodeBefore { 360 | result += "\n" + code + "\n" 361 | } 362 | result += "\n" 363 | } 364 | 365 | for _, enumTyp := range t.enumTypes { 366 | elements := t.enums[enumTyp.Type] 367 | typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements) 368 | if err != nil { 369 | return "", err 370 | } 371 | result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") 372 | } 373 | 374 | for _, strctTyp := range t.structTypes { 375 | typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode) 376 | if err != nil { 377 | return "", err 378 | } 379 | result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") 380 | } 381 | 382 | if len(t.customCodeAfter) > 0 { 383 | result += "\n" 384 | for _, code := range t.customCodeAfter { 385 | result += "\n" + code + "\n" 386 | } 387 | result += "\n" 388 | } 389 | 390 | return result, nil 391 | } 392 | 393 | func loadCustomCode(fileName string) (map[string]string, error) { 394 | result := make(map[string]string) 395 | f, err := os.Open(fileName) 396 | if err != nil { 397 | if os.IsNotExist(err) { 398 | return result, nil 399 | } 400 | return result, err 401 | } 402 | defer f.Close() 403 | 404 | bytes, err := io.ReadAll(f) 405 | if err != nil { 406 | return result, err 407 | } 408 | 409 | var currentName string 410 | var currentValue string 411 | lines := strings.Split(string(bytes), "\n") 412 | for _, line := range lines { 413 | trimmedLine := strings.TrimSpace(line) 414 | if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") { 415 | currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1) 416 | currentValue = "" 417 | } else if trimmedLine == "//[end]" { 418 | result[currentName] = strings.TrimRight(currentValue, " \t\r\n") 419 | currentName = "" 420 | currentValue = "" 421 | } else if len(currentName) > 0 { 422 | currentValue += line + "\n" 423 | } 424 | } 425 | 426 | return result, nil 427 | } 428 | 429 | func (t TypeScriptify) backup(fileName string) error { 430 | fileIn, err := os.Open(fileName) 431 | if err != nil { 432 | if !os.IsNotExist(err) { 433 | return err 434 | } 435 | // No neet to backup, just return: 436 | return nil 437 | } 438 | defer fileIn.Close() 439 | 440 | bytes, err := io.ReadAll(fileIn) 441 | if err != nil { 442 | return err 443 | } 444 | 445 | _, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99"))) 446 | if t.BackupDir != "" { 447 | backupFn = path.Join(t.BackupDir, backupFn) 448 | } 449 | 450 | return os.WriteFile(backupFn, bytes, os.FileMode(0700)) 451 | } 452 | 453 | func (t TypeScriptify) ConvertToFile(fileName string) error { 454 | if len(t.BackupDir) > 0 { 455 | err := t.backup(fileName) 456 | if err != nil { 457 | return err 458 | } 459 | } 460 | 461 | customCode, err := loadCustomCode(fileName) 462 | if err != nil { 463 | return err 464 | } 465 | 466 | f, err := os.Create(fileName) 467 | if err != nil { 468 | return err 469 | } 470 | defer f.Close() 471 | 472 | converted, err := t.Convert(customCode) 473 | if err != nil { 474 | return err 475 | } 476 | 477 | if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil { 478 | return err 479 | } 480 | if _, err := f.WriteString(converted); err != nil { 481 | return err 482 | } 483 | if err != nil { 484 | return err 485 | } 486 | 487 | return nil 488 | } 489 | 490 | type TSNamer interface { 491 | TSName() string 492 | } 493 | 494 | func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) { 495 | t.logf(depth, "Converting enum %s", typeOf.String()) 496 | if _, found := t.alreadyConverted[typeOf]; found { // Already converted 497 | return "", nil 498 | } 499 | t.alreadyConverted[typeOf] = true 500 | 501 | entityName := t.Prefix + typeOf.Name() + t.Suffix 502 | result := "enum " + entityName + " {\n" 503 | 504 | for _, val := range elements { 505 | result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value) 506 | } 507 | 508 | result += "}" 509 | 510 | if !t.DontExport { 511 | result = "export " + result 512 | } 513 | 514 | return result, nil 515 | } 516 | 517 | func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions { 518 | // By default use options defined by tags: 519 | opts := TypeOptions{ 520 | TSTransform: field.Tag.Get(tsTransformTag), 521 | TSType: field.Tag.Get(tsType), 522 | TSDoc: field.Tag.Get(tsDocTag), 523 | } 524 | 525 | overrides := []TypeOptions{} 526 | 527 | // But there is maybe an struct-specific override: 528 | for _, strct := range t.structTypes { 529 | if strct.FieldOptions == nil { 530 | continue 531 | } 532 | if strct.Type == structType { 533 | if fldOpts, found := strct.FieldOptions[field.Type]; found { 534 | overrides = append(overrides, fldOpts) 535 | } 536 | } 537 | } 538 | 539 | if fldOpts, found := t.fieldTypeOptions[field.Type]; found { 540 | overrides = append(overrides, fldOpts) 541 | } 542 | 543 | for _, o := range overrides { 544 | if o.TSTransform != "" { 545 | opts.TSTransform = o.TSTransform 546 | } 547 | if o.TSType != "" { 548 | opts.TSType = o.TSType 549 | } 550 | } 551 | 552 | return opts 553 | } 554 | 555 | func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string { 556 | jsonFieldName := "" 557 | tag := jsonTag 558 | if t.CustomJsonTag != "" { 559 | tag = t.CustomJsonTag 560 | } 561 | jsonTag := field.Tag.Get(tag) 562 | if len(jsonTag) > 0 { 563 | jsonTagParts := strings.Split(jsonTag, ",") 564 | if len(jsonTagParts) > 0 { 565 | jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent) 566 | } 567 | hasOmitEmpty := false 568 | ignored := false 569 | for _, t := range jsonTagParts { 570 | if t == "" { 571 | break 572 | } 573 | if t == "omitempty" { 574 | hasOmitEmpty = true 575 | break 576 | } 577 | if t == "-" { 578 | ignored = true 579 | break 580 | } 581 | } 582 | if !ignored && isPtr || hasOmitEmpty { 583 | jsonFieldName = fmt.Sprintf("%s?", jsonFieldName) 584 | } 585 | } else if /*field.IsExported()*/ field.PkgPath == "" { 586 | jsonFieldName = field.Name 587 | } 588 | return jsonFieldName 589 | } 590 | 591 | func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) { 592 | if _, found := t.alreadyConverted[typeOf]; found { // Already converted 593 | return "", nil 594 | } 595 | t.logf(depth, "Converting type %s", typeOf.String()) 596 | 597 | t.alreadyConverted[typeOf] = true 598 | 599 | entityName := t.Prefix + typeOf.Name() + t.Suffix 600 | result := "" 601 | if t.CreateInterface { 602 | result += fmt.Sprintf("interface %s {\n", entityName) 603 | } else { 604 | result += fmt.Sprintf("class %s {\n", entityName) 605 | } 606 | if !t.DontExport { 607 | result = "export " + result 608 | } 609 | builder := typeScriptClassBuilder{ 610 | types: t.kinds, 611 | indent: t.Indent, 612 | prefix: t.Prefix, 613 | suffix: t.Suffix, 614 | } 615 | 616 | fields := deepFields(typeOf) 617 | for _, field := range fields { 618 | isPtr := field.Type.Kind() == reflect.Ptr 619 | if isPtr { 620 | field.Type = field.Type.Elem() 621 | } 622 | jsonFieldName := t.getJSONFieldName(field, isPtr) 623 | if len(jsonFieldName) == 0 || jsonFieldName == "-" { 624 | continue 625 | } 626 | 627 | var err error 628 | fldOpts := t.getFieldOptions(typeOf, field) 629 | if fldOpts.TSDoc != "" { 630 | builder.addFieldDefinitionLine("/** " + fldOpts.TSDoc + " */") 631 | } 632 | if fldOpts.TSTransform != "" { 633 | t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) 634 | err = builder.AddSimpleField(jsonFieldName, field, fldOpts) 635 | } else if _, isEnum := t.enums[field.Type]; isEnum { 636 | t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name) 637 | builder.AddEnumField(jsonFieldName, field) 638 | } else if fldOpts.TSType != "" { // Struct: 639 | t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) 640 | err = builder.AddSimpleField(jsonFieldName, field, fldOpts) 641 | } else if field.Type.Kind() == reflect.Struct { // Struct: 642 | t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) 643 | typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode) 644 | if err != nil { 645 | return "", err 646 | } 647 | if typeScriptChunk != "" { 648 | result = typeScriptChunk + "\n" + result 649 | } 650 | builder.AddStructField(jsonFieldName, field) 651 | } else if field.Type.Kind() == reflect.Map { 652 | t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name) 653 | // Also convert map key types if needed 654 | var keyTypeToConvert reflect.Type 655 | switch field.Type.Key().Kind() { 656 | case reflect.Struct: 657 | keyTypeToConvert = field.Type.Key() 658 | case reflect.Ptr: 659 | keyTypeToConvert = field.Type.Key().Elem() 660 | } 661 | if keyTypeToConvert != nil { 662 | typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode) 663 | if err != nil { 664 | return "", err 665 | } 666 | if typeScriptChunk != "" { 667 | result = typeScriptChunk + "\n" + result 668 | } 669 | } 670 | // Also convert map value types if needed 671 | var valueTypeToConvert reflect.Type 672 | switch field.Type.Elem().Kind() { 673 | case reflect.Struct: 674 | valueTypeToConvert = field.Type.Elem() 675 | case reflect.Ptr: 676 | valueTypeToConvert = field.Type.Elem().Elem() 677 | } 678 | if valueTypeToConvert != nil { 679 | typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode) 680 | if err != nil { 681 | return "", err 682 | } 683 | if typeScriptChunk != "" { 684 | result = typeScriptChunk + "\n" + result 685 | } 686 | } 687 | 688 | builder.AddMapField(jsonFieldName, field) 689 | } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice: 690 | if field.Type.Elem().Kind() == reflect.Ptr { //extract ptr type 691 | field.Type = field.Type.Elem() 692 | } 693 | 694 | arrayDepth := 1 695 | for field.Type.Elem().Kind() == reflect.Slice { // Slice of slices: 696 | field.Type = field.Type.Elem() 697 | arrayDepth++ 698 | } 699 | 700 | if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs: 701 | t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) 702 | typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode) 703 | if err != nil { 704 | return "", err 705 | } 706 | if typeScriptChunk != "" { 707 | result = typeScriptChunk + "\n" + result 708 | } 709 | builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth) 710 | } else { // Slice of simple fields: 711 | t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name) 712 | err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts) 713 | } 714 | } else { // Simple field: 715 | t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) 716 | err = builder.AddSimpleField(jsonFieldName, field, fldOpts) 717 | } 718 | if err != nil { 719 | return "", err 720 | } 721 | } 722 | 723 | if t.CreateFromMethod { 724 | t.CreateConstructor = true 725 | } 726 | 727 | result += strings.Join(builder.fields, "\n") + "\n" 728 | if !t.CreateInterface { 729 | constructorBody := strings.Join(builder.constructorBody, "\n") 730 | needsConvertValue := strings.Contains(constructorBody, "this.convertValues") 731 | if t.CreateFromMethod { 732 | result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent) 733 | result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName) 734 | result += fmt.Sprintf("%s}\n", t.Indent) 735 | } 736 | if t.CreateConstructor { 737 | result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent) 738 | result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n" 739 | result += constructorBody + "\n" 740 | result += fmt.Sprintf("%s}\n", t.Indent) 741 | } 742 | if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) { 743 | result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n" 744 | } 745 | } 746 | 747 | if customCode != nil { 748 | code := customCode[entityName] 749 | if len(code) != 0 { 750 | result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n" 751 | } 752 | } 753 | 754 | result += "}" 755 | 756 | return result, nil 757 | } 758 | 759 | func (t *TypeScriptify) AddImport(i string) { 760 | for _, cimport := range t.customImports { 761 | if cimport == i { 762 | return 763 | } 764 | } 765 | 766 | t.customImports = append(t.customImports, i) 767 | } 768 | 769 | func (t *TypeScriptify) WithCustomCodeBefore(i string) { 770 | t.customCodeBefore = append(t.customCodeBefore, i) 771 | } 772 | 773 | func (t *TypeScriptify) WithCustomCodeAfter(i string) { 774 | t.customCodeAfter = append(t.customCodeAfter, i) 775 | } 776 | 777 | type typeScriptClassBuilder struct { 778 | types map[reflect.Kind]string 779 | indent string 780 | fields []string 781 | createFromMethodBody []string 782 | constructorBody []string 783 | prefix, suffix string 784 | } 785 | 786 | func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error { 787 | fieldType, kind := field.Type.Elem().Name(), field.Type.Elem().Kind() 788 | typeScriptType := t.types[kind] 789 | 790 | if len(fieldName) > 0 { 791 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "") 792 | if len(opts.TSType) > 0 { 793 | t.addField(fieldName, opts.TSType) 794 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) 795 | return nil 796 | } else if len(typeScriptType) > 0 { 797 | t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth))) 798 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) 799 | return nil 800 | } 801 | } 802 | 803 | return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) 804 | } 805 | 806 | func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error { 807 | fieldType, kind := field.Type.Name(), field.Type.Kind() 808 | 809 | typeScriptType := t.types[kind] 810 | if len(opts.TSType) > 0 { 811 | typeScriptType = opts.TSType 812 | } 813 | 814 | if len(typeScriptType) > 0 && len(fieldName) > 0 { 815 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "") 816 | t.addField(fieldName, typeScriptType) 817 | if opts.TSTransform == "" { 818 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) 819 | } else { 820 | val := fmt.Sprintf(`source["%s"]`, strippedFieldName) 821 | expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1) 822 | t.addInitializerFieldLine(strippedFieldName, expression) 823 | } 824 | return nil 825 | } 826 | 827 | return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) 828 | } 829 | 830 | func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) { 831 | fieldType := field.Type.Name() 832 | t.addField(fieldName, t.prefix+fieldType+t.suffix) 833 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "") 834 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) 835 | } 836 | 837 | func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField) { 838 | fieldType := field.Type.Name() 839 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "") 840 | t.addField(fieldName, t.prefix+fieldType+t.suffix) 841 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) 842 | } 843 | 844 | func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) { 845 | fieldType := field.Type.Elem().Name() 846 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "") 847 | t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth))) 848 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) 849 | } 850 | 851 | func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) { 852 | t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result.", fld, " = ", initializer, ";")) 853 | t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this.", fld, " = ", initializer, ";")) 854 | } 855 | 856 | func (t *typeScriptClassBuilder) addFieldDefinitionLine(line string) { 857 | t.fields = append(t.fields, t.indent+line) 858 | } 859 | 860 | func (t *typeScriptClassBuilder) addField(fld, fldType string) { 861 | t.fields = append(t.fields, fmt.Sprint(t.indent, fld, ": ", fldType, ";")) 862 | } 863 | -------------------------------------------------------------------------------- /typescriptify/typescriptify_test.go: -------------------------------------------------------------------------------- 1 | package typescriptify 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | type Address struct { 17 | // Used in html 18 | Duration float64 `json:"duration" custom:"durationCustom"` 19 | Text1 string `json:"text,omitempty" custom:"textCustom,omitempty"` 20 | // Ignored: 21 | Text2 string `json:",omitempty" custom:",omitempty"` 22 | Text3 string `json:"-" custom:"-"` 23 | } 24 | 25 | type Dummy struct { 26 | Something string `json:"something"` 27 | } 28 | 29 | type HasName struct { 30 | Name string `json:"name"` 31 | } 32 | 33 | type Person struct { 34 | HasName 35 | Nicknames []string `json:"nicknames"` 36 | Addresses []Address `json:"addresses"` 37 | Address *Address `json:"address"` 38 | Metadata string `json:"metadata" ts_type:"{[key:string]:string}" ts_transform:"JSON.parse(__VALUE__ || \"{}\")"` 39 | Friends []*Person `json:"friends"` 40 | Dummy Dummy `json:"a"` 41 | } 42 | 43 | func TestTypescriptifyWithTypes(t *testing.T) { 44 | t.Parallel() 45 | converter := New() 46 | 47 | converter.AddType(reflect.TypeOf(Person{})) 48 | converter.CreateConstructor = false 49 | converter.BackupDir = "" 50 | 51 | desiredResult := `export class Dummy { 52 | something: string; 53 | } 54 | export class Address { 55 | duration: number; 56 | text?: string; 57 | } 58 | export class Person { 59 | name: string; 60 | nicknames: string[]; 61 | addresses: Address[]; 62 | address?: Address; 63 | metadata: {[key:string]:string}; 64 | friends: Person[]; 65 | a: Dummy; 66 | }` 67 | testConverter(t, converter, false, desiredResult, nil) 68 | } 69 | 70 | func TestTypescriptifyWithCustomCode(t *testing.T) { 71 | t.Parallel() 72 | converter := New() 73 | 74 | converter.WithCustomCodeBefore(`"a" 75 | "b" 76 | "b"`) 77 | converter.WithCustomCodeAfter(`"d" 78 | "e" 79 | "f"`) 80 | converter.AddType(reflect.TypeOf(Person{})) 81 | converter.CreateConstructor = false 82 | converter.BackupDir = "" 83 | 84 | desiredResult := `"a" 85 | "b" 86 | "b" 87 | 88 | 89 | export class Dummy { 90 | something: string; 91 | } 92 | export class Address { 93 | duration: number; 94 | text?: string; 95 | } 96 | export class Person { 97 | name: string; 98 | nicknames: string[]; 99 | addresses: Address[]; 100 | address?: Address; 101 | metadata: {[key:string]:string}; 102 | friends: Person[]; 103 | a: Dummy; 104 | } 105 | 106 | "d" 107 | "e" 108 | "f"` 109 | testConverter(t, converter, false, desiredResult, nil) 110 | } 111 | 112 | func TestTypescriptifyWithCustomImports(t *testing.T) { 113 | t.Parallel() 114 | converter := New() 115 | 116 | converter.AddType(reflect.TypeOf(Person{})) 117 | converter.BackupDir = "" 118 | converter.AddImport("//import { Decimal } from 'decimal.js'") 119 | converter.CreateConstructor = false 120 | 121 | desiredResult := ` 122 | //import { Decimal } from 'decimal.js' 123 | 124 | export class Dummy { 125 | something: string; 126 | } 127 | export class Address { 128 | duration: number; 129 | text?: string; 130 | } 131 | export class Person { 132 | name: string; 133 | nicknames: string[]; 134 | addresses: Address[]; 135 | address?: Address; 136 | metadata: {[key:string]:string}; 137 | friends: Person[]; 138 | a: Dummy; 139 | }` 140 | testConverter(t, converter, false, desiredResult, nil) 141 | } 142 | 143 | func TestTypescriptifyWithInstances(t *testing.T) { 144 | t.Parallel() 145 | converter := New() 146 | 147 | converter.Add(Person{}) 148 | converter.Add(Dummy{}) 149 | converter.DontExport = true 150 | converter.BackupDir = "" 151 | converter.CreateConstructor = false 152 | 153 | desiredResult := `class Dummy { 154 | something: string; 155 | } 156 | class Address { 157 | duration: number; 158 | text?: string; 159 | } 160 | class Person { 161 | name: string; 162 | nicknames: string[]; 163 | addresses: Address[]; 164 | address?: Address; 165 | metadata: {[key:string]:string}; 166 | friends: Person[]; 167 | a: Dummy; 168 | }` 169 | testConverter(t, converter, false, desiredResult, nil) 170 | } 171 | 172 | func TestTypescriptifyWithInterfaces(t *testing.T) { 173 | t.Parallel() 174 | converter := New() 175 | 176 | converter.Add(Person{}) 177 | converter.Add(Dummy{}) 178 | converter.DontExport = true 179 | converter.BackupDir = "" 180 | converter.CreateInterface = true 181 | 182 | desiredResult := `interface Dummy { 183 | something: string; 184 | } 185 | interface Address { 186 | duration: number; 187 | text?: string; 188 | } 189 | interface Person { 190 | name: string; 191 | nicknames: string[]; 192 | addresses: Address[]; 193 | address?: Address; 194 | metadata: {[key:string]:string}; 195 | friends: Person[]; 196 | a: Dummy; 197 | }` 198 | testConverter(t, converter, true, desiredResult, nil) 199 | } 200 | 201 | func TestTypescriptifyWithDoubleClasses(t *testing.T) { 202 | t.Parallel() 203 | converter := New() 204 | 205 | converter.AddType(reflect.TypeOf(Person{})) 206 | converter.AddType(reflect.TypeOf(Person{})) 207 | converter.CreateConstructor = false 208 | converter.BackupDir = "" 209 | 210 | desiredResult := `export class Dummy { 211 | something: string; 212 | } 213 | export class Address { 214 | duration: number; 215 | text?: string; 216 | } 217 | export class Person { 218 | name: string; 219 | nicknames: string[]; 220 | addresses: Address[]; 221 | address?: Address; 222 | metadata: {[key:string]:string}; 223 | friends: Person[]; 224 | a: Dummy; 225 | }` 226 | testConverter(t, converter, false, desiredResult, nil) 227 | } 228 | 229 | func TestWithPrefixes(t *testing.T) { 230 | t.Parallel() 231 | converter := New() 232 | 233 | converter.Prefix = "test_" 234 | converter.Suffix = "_test" 235 | 236 | converter.Add(Person{}) 237 | converter.DontExport = true 238 | converter.BackupDir = "" 239 | 240 | desiredResult := `class test_Dummy_test { 241 | something: string; 242 | 243 | constructor(source: any = {}) { 244 | if ('string' === typeof source) source = JSON.parse(source); 245 | this.something = source["something"]; 246 | } 247 | } 248 | class test_Address_test { 249 | duration: number; 250 | text?: string; 251 | 252 | constructor(source: any = {}) { 253 | if ('string' === typeof source) source = JSON.parse(source); 254 | this.duration = source["duration"]; 255 | this.text = source["text"]; 256 | } 257 | } 258 | class test_Person_test { 259 | name: string; 260 | nicknames: string[]; 261 | addresses: test_Address_test[]; 262 | address?: test_Address_test; 263 | metadata: {[key:string]:string}; 264 | friends: test_Person_test[]; 265 | a: test_Dummy_test; 266 | 267 | constructor(source: any = {}) { 268 | if ('string' === typeof source) source = JSON.parse(source); 269 | this.name = source["name"]; 270 | this.nicknames = source["nicknames"]; 271 | this.addresses = this.convertValues(source["addresses"], test_Address_test); 272 | this.address = this.convertValues(source["address"], test_Address_test); 273 | this.metadata = JSON.parse(source["metadata"] || "{}"); 274 | this.friends = this.convertValues(source["friends"], test_Person_test); 275 | this.a = this.convertValues(source["a"], test_Dummy_test); 276 | } 277 | 278 | ` + tsConvertValuesFunc + ` 279 | }` 280 | jsn := jsonizeOrPanic(Person{ 281 | Address: &Address{Text1: "txt1"}, 282 | Addresses: []Address{{Text1: "111"}}, 283 | Metadata: `{"something": "aaa"}`, 284 | }) 285 | testConverter(t, converter, true, desiredResult, []string{ 286 | `new test_Person_test()`, 287 | `JSON.stringify(new test_Person_test()?.metadata) === "{}"`, 288 | `!(new test_Person_test()?.address)`, 289 | `!(new test_Person_test()?.addresses)`, 290 | `!(new test_Person_test()?.addresses)`, 291 | 292 | `new test_Person_test(` + jsn + ` as any)`, 293 | `new test_Person_test(` + jsn + ` as any)?.metadata?.something === "aaa"`, 294 | `(new test_Person_test(` + jsn + ` as any)?.address as test_Address_test).text === "txt1"`, 295 | `new test_Person_test(` + jsn + ` as any)?.addresses?.length === 1`, 296 | `(new test_Person_test(` + jsn + ` as any)?.addresses[0] as test_Address_test)?.text === "111"`, 297 | }) 298 | } 299 | 300 | func testConverter(t *testing.T, converter *TypeScriptify, strictMode bool, desiredResult string, tsExpressionAndDesiredResults []string) { 301 | typeScriptCode, err := converter.Convert(nil) 302 | if err != nil { 303 | panic(err.Error()) 304 | } 305 | 306 | fmt.Println("----------------------------------------------------------------------------------------------------") 307 | fmt.Println(desiredResult) 308 | fmt.Println("----------------------------------------------------------------------------------------------------") 309 | fmt.Println(typeScriptCode) 310 | fmt.Println("----------------------------------------------------------------------------------------------------") 311 | 312 | desiredResult = strings.TrimSpace(desiredResult) 313 | typeScriptCode = strings.Trim(typeScriptCode, " \t\n\r") 314 | if typeScriptCode != desiredResult { 315 | gotLines1 := strings.Split(typeScriptCode, "\n") 316 | expectedLines2 := strings.Split(desiredResult, "\n") 317 | 318 | max := len(gotLines1) 319 | if len(expectedLines2) > max { 320 | max = len(expectedLines2) 321 | } 322 | 323 | for i := 0; i < max; i++ { 324 | var gotLine, expectedLine string 325 | if i < len(gotLines1) { 326 | gotLine = gotLines1[i] 327 | } 328 | if i < len(expectedLines2) { 329 | expectedLine = expectedLines2[i] 330 | } 331 | if assert.Equal(t, strings.TrimSpace(expectedLine), strings.TrimSpace(gotLine), "line #%d", 1+i) { 332 | fmt.Printf("OK: %s\n", gotLine) 333 | } else { 334 | t.FailNow() 335 | } 336 | } 337 | } 338 | 339 | if t.Failed() { 340 | t.FailNow() 341 | } 342 | 343 | testTypescriptExpression(t, strictMode, typeScriptCode, tsExpressionAndDesiredResults) 344 | } 345 | 346 | func testTypescriptExpression(t *testing.T, strictMode bool, baseScript string, tsExpressionAndDesiredResults []string) { 347 | f, err := os.CreateTemp(os.TempDir(), "*.ts") 348 | assert.Nil(t, err) 349 | assert.NotNil(t, f) 350 | 351 | if t.Failed() { 352 | t.FailNow() 353 | } 354 | 355 | _, _ = f.WriteString(baseScript) 356 | _, _ = f.WriteString("\n") 357 | for n, expr := range tsExpressionAndDesiredResults { 358 | _, _ = f.WriteString("// " + expr + "\n") 359 | _, _ = f.WriteString(`if (` + expr + `) { console.log("#` + fmt.Sprint(1+n) + ` OK") } else { throw new Error() }`) 360 | _, _ = f.WriteString("\n\n") 361 | } 362 | 363 | fmt.Println("tmp ts: ", f.Name()) 364 | var byts []byte 365 | if strictMode { 366 | byts, err = exec.Command("tsc", "--strict", f.Name()).CombinedOutput() 367 | } else { 368 | byts, err = exec.Command("tsc", f.Name()).CombinedOutput() 369 | } 370 | assert.Nil(t, err, string(byts)) 371 | 372 | jsFile := strings.Replace(f.Name(), ".ts", ".js", 1) 373 | fmt.Println("executing:", jsFile) 374 | byts, err = exec.Command("node", jsFile).CombinedOutput() 375 | assert.Nil(t, err, string(byts)) 376 | } 377 | 378 | func TestTypescriptifyCustomType(t *testing.T) { 379 | t.Parallel() 380 | type TestCustomType struct { 381 | Map map[string]int `json:"map" ts_type:"{[key: string]: number}"` 382 | } 383 | 384 | converter := New() 385 | 386 | converter.AddType(reflect.TypeOf(TestCustomType{})) 387 | converter.BackupDir = "" 388 | converter.CreateConstructor = false 389 | 390 | desiredResult := `export class TestCustomType { 391 | map: {[key: string]: number}; 392 | }` 393 | testConverter(t, converter, false, desiredResult, nil) 394 | } 395 | 396 | func TestDate(t *testing.T) { 397 | t.Parallel() 398 | type TestCustomType struct { 399 | Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"` 400 | } 401 | 402 | converter := New() 403 | converter.AddType(reflect.TypeOf(TestCustomType{})) 404 | converter.BackupDir = "" 405 | 406 | desiredResult := `export class TestCustomType { 407 | time: Date; 408 | 409 | constructor(source: any = {}) { 410 | if ('string' === typeof source) source = JSON.parse(source); 411 | this.time = new Date(source["time"]); 412 | } 413 | }` 414 | 415 | jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)}) 416 | testConverter(t, converter, true, desiredResult, []string{ 417 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`, 418 | //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`, 419 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`, 420 | }) 421 | } 422 | 423 | func TestDateWithoutTags(t *testing.T) { 424 | t.Parallel() 425 | type TestCustomType struct { 426 | Time time.Time `json:"time"` 427 | } 428 | 429 | // Test with custom field options defined per-one-struct: 430 | converter1 := New() 431 | converter1.Add(NewStruct(TestCustomType{}).WithFieldOpts(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"})) 432 | converter1.BackupDir = "" 433 | 434 | // Test with custom field options defined globally: 435 | converter2 := New() 436 | converter2.Add(reflect.TypeOf(TestCustomType{})) 437 | converter2.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"}) 438 | converter2.BackupDir = "" 439 | 440 | for _, converter := range []*TypeScriptify{converter1, converter2} { 441 | desiredResult := `export class TestCustomType { 442 | time: Date; 443 | 444 | constructor(source: any = {}) { 445 | if ('string' === typeof source) source = JSON.parse(source); 446 | this.time = new Date(source["time"]); 447 | } 448 | }` 449 | 450 | jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)}) 451 | testConverter(t, converter, true, desiredResult, []string{ 452 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`, 453 | //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`, 454 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`, 455 | }) 456 | } 457 | } 458 | 459 | func TestRecursive(t *testing.T) { 460 | t.Parallel() 461 | type Test struct { 462 | Children []Test `json:"children"` 463 | } 464 | 465 | converter := New() 466 | 467 | converter.AddType(reflect.TypeOf(Test{})) 468 | converter.BackupDir = "" 469 | 470 | desiredResult := `export class Test { 471 | children: Test[]; 472 | 473 | constructor(source: any = {}) { 474 | if ('string' === typeof source) source = JSON.parse(source); 475 | this.children = this.convertValues(source["children"], Test); 476 | } 477 | 478 | ` + tsConvertValuesFunc + ` 479 | }` 480 | testConverter(t, converter, true, desiredResult, nil) 481 | } 482 | 483 | func TestArrayOfArrays(t *testing.T) { 484 | t.Parallel() 485 | type Key struct { 486 | Key string `json:"key"` 487 | } 488 | type Keyboard struct { 489 | Keys [][]Key `json:"keys"` 490 | } 491 | 492 | converter := New() 493 | 494 | converter.AddType(reflect.TypeOf(Keyboard{})) 495 | converter.BackupDir = "" 496 | 497 | desiredResult := `export class Key { 498 | key: string; 499 | 500 | constructor(source: any = {}) { 501 | if ('string' === typeof source) source = JSON.parse(source); 502 | this.key = source["key"]; 503 | } 504 | } 505 | export class Keyboard { 506 | keys: Key[][]; 507 | 508 | constructor(source: any = {}) { 509 | if ('string' === typeof source) source = JSON.parse(source); 510 | this.keys = this.convertValues(source["keys"], Key); 511 | } 512 | 513 | ` + tsConvertValuesFunc + ` 514 | }` 515 | testConverter(t, converter, true, desiredResult, nil) 516 | } 517 | 518 | func TestFixedArray(t *testing.T) { 519 | t.Parallel() 520 | type Sub struct{} 521 | type Tmp struct { 522 | Arr [3]string `json:"arr"` 523 | Arr2 [3]Sub `json:"arr2"` 524 | } 525 | 526 | converter := New() 527 | 528 | converter.AddType(reflect.TypeOf(Tmp{})) 529 | converter.BackupDir = "" 530 | 531 | desiredResult := `export class Sub { 532 | 533 | 534 | constructor(source: any = {}) { 535 | if ('string' === typeof source) source = JSON.parse(source); 536 | 537 | } 538 | } 539 | export class Tmp { 540 | arr: string[]; 541 | arr2: Sub[]; 542 | 543 | constructor(source: any = {}) { 544 | if ('string' === typeof source) source = JSON.parse(source); 545 | this.arr = source["arr"]; 546 | this.arr2 = this.convertValues(source["arr2"], Sub); 547 | } 548 | 549 | ` + tsConvertValuesFunc + ` 550 | } 551 | ` 552 | testConverter(t, converter, true, desiredResult, nil) 553 | } 554 | 555 | func TestAny(t *testing.T) { 556 | t.Parallel() 557 | type Test struct { 558 | Any interface{} `json:"field"` 559 | } 560 | 561 | converter := New() 562 | 563 | converter.AddType(reflect.TypeOf(Test{})) 564 | converter.BackupDir = "" 565 | 566 | desiredResult := `export class Test { 567 | field: any; 568 | 569 | constructor(source: any = {}) { 570 | if ('string' === typeof source) source = JSON.parse(source); 571 | this.field = source["field"]; 572 | } 573 | }` 574 | testConverter(t, converter, true, desiredResult, nil) 575 | } 576 | 577 | type NumberTime time.Time 578 | 579 | func (t NumberTime) MarshalJSON() ([]byte, error) { 580 | return []byte(fmt.Sprintf("%d", time.Time(t).Unix())), nil 581 | } 582 | 583 | func TestTypeAlias(t *testing.T) { 584 | t.Parallel() 585 | type Person struct { 586 | Birth NumberTime `json:"birth" ts_type:"number"` 587 | } 588 | 589 | converter := New() 590 | 591 | converter.AddType(reflect.TypeOf(Person{})) 592 | converter.BackupDir = "" 593 | converter.CreateConstructor = false 594 | 595 | desiredResult := `export class Person { 596 | birth: number; 597 | }` 598 | testConverter(t, converter, false, desiredResult, nil) 599 | } 600 | 601 | type MSTime struct { 602 | time.Time 603 | } 604 | 605 | func (MSTime) UnmarshalJSON([]byte) error { return nil } 606 | func (MSTime) MarshalJSON() ([]byte, error) { return []byte("1111"), nil } 607 | 608 | func TestOverrideCustomType(t *testing.T) { 609 | t.Parallel() 610 | 611 | type SomeStruct struct { 612 | Time MSTime `json:"time" ts_type:"number"` 613 | } 614 | var _ json.Marshaler = new(MSTime) 615 | var _ json.Unmarshaler = new(MSTime) 616 | 617 | converter := New() 618 | 619 | converter.AddType(reflect.TypeOf(SomeStruct{})) 620 | converter.BackupDir = "" 621 | converter.CreateConstructor = false 622 | 623 | desiredResult := `export class SomeStruct { 624 | time: number; 625 | }` 626 | testConverter(t, converter, false, desiredResult, nil) 627 | 628 | byts, _ := json.Marshal(SomeStruct{Time: MSTime{Time: time.Now()}}) 629 | assert.Equal(t, `{"time":1111}`, string(byts)) 630 | } 631 | 632 | type Weekday int 633 | 634 | const ( 635 | Sunday Weekday = iota 636 | Monday 637 | Tuesday 638 | Wednesday 639 | Thursday 640 | Friday 641 | Saturday 642 | ) 643 | 644 | func (w Weekday) TSName() string { 645 | switch w { 646 | case Sunday: 647 | return "SUNDAY" 648 | case Monday: 649 | return "MONDAY" 650 | case Tuesday: 651 | return "TUESDAY" 652 | case Wednesday: 653 | return "WEDNESDAY" 654 | case Thursday: 655 | return "THURSDAY" 656 | case Friday: 657 | return "FRIDAY" 658 | case Saturday: 659 | return "SATURDAY" 660 | default: 661 | return "???" 662 | } 663 | } 664 | 665 | // One way to specify enums is to list all values and then every one must have a TSName() method 666 | var allWeekdaysV1 = []Weekday{ 667 | Sunday, 668 | Monday, 669 | Tuesday, 670 | Wednesday, 671 | Thursday, 672 | Friday, 673 | Saturday, 674 | } 675 | 676 | // Another way to specify enums: 677 | var allWeekdaysV2 = []struct { 678 | Value Weekday 679 | TSName string 680 | }{ 681 | {Sunday, "SUNDAY"}, 682 | {Monday, "MONDAY"}, 683 | {Tuesday, "TUESDAY"}, 684 | {Wednesday, "WEDNESDAY"}, 685 | {Thursday, "THURSDAY"}, 686 | {Friday, "FRIDAY"}, 687 | {Saturday, "SATURDAY"}, 688 | } 689 | 690 | type Holliday struct { 691 | Name string `json:"name"` 692 | Weekday Weekday `json:"weekday"` 693 | } 694 | 695 | func TestEnum(t *testing.T) { 696 | t.Parallel() 697 | for _, allWeekdays := range []interface{}{allWeekdaysV1, allWeekdaysV2} { 698 | converter := New(). 699 | AddType(reflect.TypeOf(Holliday{})). 700 | AddEnum(allWeekdays). 701 | WithConstructor(true). 702 | WithBackupDir("") 703 | 704 | desiredResult := `export enum Weekday { 705 | SUNDAY = 0, 706 | MONDAY = 1, 707 | TUESDAY = 2, 708 | WEDNESDAY = 3, 709 | THURSDAY = 4, 710 | FRIDAY = 5, 711 | SATURDAY = 6, 712 | } 713 | export class Holliday { 714 | name: string; 715 | weekday: Weekday; 716 | 717 | constructor(source: any = {}) { 718 | if ('string' === typeof source) source = JSON.parse(source); 719 | this.name = source["name"]; 720 | this.weekday = source["weekday"]; 721 | } 722 | }` 723 | testConverter(t, converter, true, desiredResult, nil) 724 | } 725 | } 726 | 727 | type Gender string 728 | 729 | const ( 730 | MaleStr Gender = "m" 731 | FemaleStr Gender = "f" 732 | ) 733 | 734 | var allGenders = []struct { 735 | Value Gender 736 | TSName string 737 | }{ 738 | {MaleStr, "MALE"}, 739 | {FemaleStr, "FEMALE"}, 740 | } 741 | 742 | func TestEnumWithStringValues(t *testing.T) { 743 | t.Parallel() 744 | converter := New(). 745 | AddEnum(allGenders). 746 | WithConstructor(false). 747 | WithBackupDir("") 748 | 749 | desiredResult := ` 750 | export enum Gender { 751 | MALE = "m", 752 | FEMALE = "f", 753 | } 754 | ` 755 | testConverter(t, converter, true, desiredResult, nil) 756 | } 757 | 758 | func TestConstructorWithReferences(t *testing.T) { 759 | t.Parallel() 760 | converter := New(). 761 | AddType(reflect.TypeOf(Person{})). 762 | AddEnum(allWeekdaysV2). 763 | WithConstructor(true). 764 | WithBackupDir("") 765 | 766 | desiredResult := `export enum Weekday { 767 | SUNDAY = 0, 768 | MONDAY = 1, 769 | TUESDAY = 2, 770 | WEDNESDAY = 3, 771 | THURSDAY = 4, 772 | FRIDAY = 5, 773 | SATURDAY = 6, 774 | } 775 | export class Dummy { 776 | something: string; 777 | 778 | constructor(source: any = {}) { 779 | if ('string' === typeof source) source = JSON.parse(source); 780 | this.something = source["something"]; 781 | } 782 | } 783 | export class Address { 784 | duration: number; 785 | text?: string; 786 | 787 | constructor(source: any = {}) { 788 | if ('string' === typeof source) source = JSON.parse(source); 789 | this.duration = source["duration"]; 790 | this.text = source["text"]; 791 | } 792 | } 793 | export class Person { 794 | name: string; 795 | nicknames: string[]; 796 | addresses: Address[]; 797 | address?: Address; 798 | metadata: {[key:string]:string}; 799 | friends: Person[]; 800 | a: Dummy; 801 | 802 | constructor(source: any = {}) { 803 | if ('string' === typeof source) source = JSON.parse(source); 804 | this.name = source["name"]; 805 | this.nicknames = source["nicknames"]; 806 | this.addresses = this.convertValues(source["addresses"], Address); 807 | this.address = this.convertValues(source["address"], Address); 808 | this.metadata = JSON.parse(source["metadata"] || "{}"); 809 | this.friends = this.convertValues(source["friends"], Person); 810 | this.a = this.convertValues(source["a"], Dummy); 811 | } 812 | 813 | ` + tsConvertValuesFunc + ` 814 | }` 815 | testConverter(t, converter, true, desiredResult, nil) 816 | } 817 | 818 | type WithMap struct { 819 | Map map[string]int `json:"simpleMap"` 820 | MapObjects map[string]Address `json:"mapObjects"` 821 | PtrMap *map[string]Address `json:"ptrMapObjects"` 822 | } 823 | 824 | func TestMaps(t *testing.T) { 825 | t.Parallel() 826 | converter := New(). 827 | AddType(reflect.TypeOf(WithMap{})). 828 | WithConstructor(true). 829 | WithPrefix("API_"). 830 | WithBackupDir("") 831 | 832 | desiredResult := ` 833 | export class API_Address { 834 | duration: number; 835 | text?: string; 836 | 837 | constructor(source: any = {}) { 838 | if ('string' === typeof source) source = JSON.parse(source); 839 | this.duration = source["duration"]; 840 | this.text = source["text"]; 841 | } 842 | } 843 | export class API_WithMap { 844 | simpleMap: {[key: string]: number}; 845 | mapObjects: {[key: string]: API_Address}; 846 | ptrMapObjects?: {[key: string]: API_Address}; 847 | 848 | constructor(source: any = {}) { 849 | if ('string' === typeof source) source = JSON.parse(source); 850 | this.simpleMap = source["simpleMap"]; 851 | this.mapObjects = this.convertValues(source["mapObjects"], API_Address, true); 852 | this.ptrMapObjects = this.convertValues(source["ptrMapObjects"], API_Address, true); 853 | } 854 | 855 | ` + tsConvertValuesFunc + ` 856 | } 857 | ` 858 | 859 | json := WithMap{ 860 | Map: map[string]int{"aaa": 1}, 861 | MapObjects: map[string]Address{"bbb": {Duration: 1.0, Text1: "txt1"}}, 862 | PtrMap: &map[string]Address{"ccc": {Duration: 2.0, Text1: "txt2"}}, 863 | } 864 | 865 | testConverter(t, converter, true, desiredResult, []string{ 866 | `new API_WithMap(` + jsonizeOrPanic(json) + `).simpleMap.aaa == 1`, 867 | `(new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_Address`, 868 | `!((new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_WithMap)`, 869 | `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.duration == 1`, 870 | `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.text === "txt1"`, 871 | `(new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_Address`, 872 | `!((new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_WithMap)`, 873 | `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.duration === 2`, 874 | `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.text === "txt2"`, 875 | }) 876 | } 877 | 878 | func TestPTR(t *testing.T) { 879 | t.Parallel() 880 | type Person struct { 881 | Name *string `json:"name"` 882 | } 883 | 884 | converter := New() 885 | converter.BackupDir = "" 886 | converter.CreateConstructor = false 887 | converter.Add(Person{}) 888 | 889 | desiredResult := `export class Person { 890 | name?: string; 891 | }` 892 | testConverter(t, converter, true, desiredResult, nil) 893 | } 894 | 895 | type PersonWithPtrName struct { 896 | *HasName 897 | } 898 | 899 | func TestAnonymousPtr(t *testing.T) { 900 | t.Parallel() 901 | var p PersonWithPtrName 902 | p.HasName = &HasName{} 903 | p.Name = "JKLJKL" 904 | converter := New(). 905 | AddType(reflect.TypeOf(PersonWithPtrName{})). 906 | WithConstructor(true). 907 | WithBackupDir("") 908 | 909 | desiredResult := ` 910 | export class PersonWithPtrName { 911 | name: string; 912 | 913 | constructor(source: any = {}) { 914 | if ('string' === typeof source) source = JSON.parse(source); 915 | this.name = source["name"]; 916 | } 917 | } 918 | ` 919 | testConverter(t, converter, true, desiredResult, nil) 920 | } 921 | 922 | func jsonizeOrPanic(i interface{}) string { 923 | byts, err := json.Marshal(i) 924 | if err != nil { 925 | panic(err) 926 | } 927 | return string(byts) 928 | } 929 | 930 | func TestTestConverter(t *testing.T) { 931 | t.Parallel() 932 | 933 | ts := `class Converter { 934 | ` + tsConvertValuesFunc + ` 935 | } 936 | const converter = new Converter(); 937 | 938 | class Address { 939 | street: string; 940 | number: number; 941 | 942 | constructor(a: any) { 943 | this.street = a["street"]; 944 | this.number = a["number"]; 945 | } 946 | } 947 | ` 948 | 949 | testTypescriptExpression(t, true, ts, []string{ 950 | `(converter.convertValues(null, Address)) === null`, 951 | `(converter.convertValues([], Address)).length === 0`, 952 | `(converter.convertValues({}, Address)) instanceof Address`, 953 | `!(converter.convertValues({}, Address, true) instanceof Address)`, 954 | 955 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[]).length == 1`, 956 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0] instanceof Address`, 957 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].number === 19`, 958 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].street === "aaa"`, 959 | 960 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[]).length == 1`, 961 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0] instanceof Address`, 962 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].number === 19`, 963 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].street === "aaa"`, 964 | 965 | `Object.keys((converter.convertValues({"first": {street: "aaa", number: 19}}, Address, true) as {[_: string]: Address})).length == 1`, 966 | `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"] instanceof Address`, 967 | `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].number === 19`, 968 | `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].street === "aaa"`, 969 | }) 970 | } 971 | 972 | func TestIgnoredPTR(t *testing.T) { 973 | t.Parallel() 974 | type PersonWithIgnoredPtr struct { 975 | Name string `json:"name"` 976 | Nickname *string `json:"-"` 977 | } 978 | 979 | converter := New() 980 | converter.BackupDir = "" 981 | converter.Add(PersonWithIgnoredPtr{}) 982 | 983 | desiredResult := ` 984 | export class PersonWithIgnoredPtr { 985 | name: string; 986 | 987 | constructor(source: any = {}) { 988 | if ('string' === typeof source) source = JSON.parse(source); 989 | this.name = source["name"]; 990 | } 991 | } 992 | ` 993 | testConverter(t, converter, true, desiredResult, nil) 994 | } 995 | 996 | func TestMapWithPrefix(t *testing.T) { 997 | t.Parallel() 998 | 999 | type Example struct { 1000 | Variable map[string]string `json:"variable"` 1001 | } 1002 | 1003 | converter := New().WithPrefix("prefix_").Add(Example{}) 1004 | 1005 | desiredResult := ` 1006 | export class prefix_Example { 1007 | variable: {[key: string]: string}; 1008 | 1009 | constructor(source: any = {}) { 1010 | if ('string' === typeof source) source = JSON.parse(source); 1011 | this.variable = source["variable"]; 1012 | } 1013 | } 1014 | ` 1015 | testConverter(t, converter, true, desiredResult, nil) 1016 | } 1017 | 1018 | func TestFieldNamesWithoutJSONAnnotation(t *testing.T) { 1019 | t.Parallel() 1020 | 1021 | type WithoutAnnotation struct { 1022 | PublicField string 1023 | privateField string 1024 | } 1025 | var tmp WithoutAnnotation 1026 | tmp.privateField = "" 1027 | 1028 | converter := New().Add(WithoutAnnotation{}) 1029 | desiredResult := ` 1030 | export class WithoutAnnotation { 1031 | PublicField: string; 1032 | 1033 | constructor(source: any = {}) { 1034 | if ('string' === typeof source) source = JSON.parse(source); 1035 | this.PublicField = source["PublicField"]; 1036 | } 1037 | } 1038 | ` 1039 | testConverter(t, converter, true, desiredResult, nil) 1040 | } 1041 | 1042 | func TestTypescriptifyComment(t *testing.T) { 1043 | t.Parallel() 1044 | type Person struct { 1045 | Age int `json:"age" ts_doc:"Age comment"` 1046 | Name string `json:"name" ts_doc:"Name comment"` 1047 | } 1048 | 1049 | converter := New() 1050 | 1051 | converter.AddType(reflect.TypeOf(Person{})) 1052 | converter.BackupDir = "" 1053 | converter.CreateConstructor = false 1054 | 1055 | desiredResult := `export class Person { 1056 | /** Age comment */ 1057 | age: number; 1058 | /** Name comment */ 1059 | name: string; 1060 | }` 1061 | testConverter(t, converter, false, desiredResult, nil) 1062 | } 1063 | 1064 | func TestTypescriptifyCustomJsonTag(t *testing.T) { 1065 | t.Parallel() 1066 | 1067 | converter := New().WithCustomJsonTag("custom") 1068 | 1069 | converter.AddType(reflect.TypeOf(Address{})) 1070 | converter.CreateConstructor = false 1071 | converter.BackupDir = "" 1072 | 1073 | desiredResult := `export class Address { 1074 | durationCustom: number; 1075 | textCustom?: string; 1076 | }` 1077 | testConverter(t, converter, false, desiredResult, nil) 1078 | } 1079 | -------------------------------------------------------------------------------- /typescriptify/utils.go: -------------------------------------------------------------------------------- 1 | package typescriptify 2 | 3 | import "strings" 4 | 5 | func indentLines(str string, i int) string { 6 | lines := strings.Split(str, "\n") 7 | for n := range lines { 8 | lines[n] = strings.Repeat("\t", i) + lines[n] 9 | } 10 | return strings.Join(lines, "\n") 11 | } 12 | --------------------------------------------------------------------------------