diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b935686..5af0b53 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,41 +1,44 @@ -name: Go - -on: - push: - branches: '*' - pull_request: - branches: 'master' - -jobs: - build: - strategy: - fail-fast: false - matrix: - go-version: [1.16.x] - os: [ubuntu-latest, macos-latest, windows-latest] - name: Build and test - runs-on: ${{ matrix.os }} - steps: - - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go-version }} - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Get dependencies - run: | - go mod download - - - name: Build - run: | - cd tscriptify - go build -v . - - - name: Test - run: | - cd typescriptify - go test -v . +name: Go + +on: + push: + branches: "*" + pull_request: + branches: "main" + +jobs: + build: + strategy: + fail-fast: false + matrix: + go-version: [1.21.x] + os: [ubuntu-latest, macos-latest, windows-latest] + name: Build and test + runs-on: ${{ matrix.os }} + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: | + go mod download + + - name: Ensure Typescript is available + run: | + npm install -g typescript + + - name: Build + run: | + cd tscriptify + go build -v . + + - name: Test + run: | + cd typescriptify + go test -v . diff --git a/.gitignore b/.gitignore index 1a6d336..d73e2b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -.idea/ -.vscode/ -*.iml -tags -tmp_* +.idea/ +.vscode/ +*.iml +tags +tmp_* +node_modules diff --git a/README.md b/README.md index a9c512e..ed6aea6 100644 --- a/README.md +++ b/README.md @@ -1,396 +1,469 @@ -# A Golang JSON to TypeScript model converter - -## Installation - -The command-line tool: - -``` -go get github.com/tkrajina/typescriptify-golang-structs/tscriptify -``` - -The library: - -``` -go get github.com/tkrajina/typescriptify-golang-structs -``` - -## Usage - -Use the command line tool: - -``` -tscriptify -package=package/with/your/models -target=target_ts_file.ts Model1 Model2 -``` - -If you need to import a custom type in Typescript, you can pass the import string: - -``` -tscriptify -package=package/with/your/models -target=target_ts_file.ts -import="import { Decimal } from 'decimal.js'" Model1 Model2 -``` - -If all your structs are in one file, you can convert them with: - -``` -tscriptify -package=package/with/your/models -target=target_ts_file.ts path/to/file/with/structs.go -``` - -Or by using it from your code: - -```golang -converter := typescriptify.New(). - Add(Person{}). - Add(Dummy{}) -err := converter.ConvertToFile("ts/models.ts") -if err != nil { - panic(err.Error()) -} -``` - -Command line options: - -``` -$ tscriptify --help -Usage of tscriptify: --backup string - Directory where backup files are saved --package string - Path of the package with models --target string - Target typescript file -``` - -## Models and conversion - -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. - -Example input structs: - -```golang -type Address struct { - City string `json:"city"` - Number float64 `json:"number"` - Country string `json:"country,omitempty"` -} - -type PersonalInfo struct { - Hobbies []string `json:"hobby"` - PetName string `json:"pet_name"` -} - -type Person struct { - Name string `json:"name"` - PersonalInfo PersonalInfo `json:"personal_info"` - Nicknames []string `json:"nicknames"` - Addresses []Address `json:"addresses"` - Address *Address `json:"address"` - Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"` - Friends []*Person `json:"friends"` -} -``` - -Generated TypeScript: - -```typescript -export class Address { - city: string; - number: number; - country?: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.city = source["city"]; - this.number = source["number"]; - this.country = source["country"]; - } -} -export class PersonalInfo { - hobby: string[]; - pet_name: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.hobby = source["hobby"]; - this.pet_name = source["pet_name"]; - } -} -export class Person { - name: string; - personal_info: PersonalInfo; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.name = source["name"]; - this.personal_info = this.convertValues(source["personal_info"], PersonalInfo); - this.nicknames = source["nicknames"]; - this.addresses = this.convertValues(source["addresses"], Address); - this.address = this.convertValues(source["address"], Address); - this.metadata = source["metadata"]; - this.friends = this.convertValues(source["friends"], Person); - } - - convertValues(a: any, classs: any, asMap: boolean = false): any { - if (!a) { - return a; - } - if (a.slice) { - return (a as any[]).map(elem => this.convertValues(elem, classs)); - } else if ("object" === typeof a) { - if (asMap) { - for (const key of Object.keys(a)) { - a[key] = new classs(a[key]); - } - return a; - } - return new classs(a); - } - return a; - } -} -``` - -If you prefer interfaces, the output is: - -```typescript -export interface Address { - city: string; - number: number; - country?: string; -} -export interface PersonalInfo { - hobby: string[]; - pet_name: string; -} -export interface Person { - name: string; - personal_info: PersonalInfo; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; -} -``` - -In TypeScript you can just cast your json object in any of those models: - -```typescript -var person = {"name":"Me myself","nicknames":["aaa", "bbb"]}; -console.log(person.name); -// The TypeScript compiler will throw an error for this line -console.log(person.something); -``` - -## Custom Typescript code - -Any custom code can be added to Typescript models: - -```typescript -class Address { - street : string; - no : number; - //[Address:] - country: string; - getStreetAndNumber() { - return street + " " + number; - } - //[end] -} -``` - -The lines between `//[Address:]` and `//[end]` will be left intact after `ConvertToFile()`. - -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. - -In that case use the constructor: - -```typescript -var person = new Person({"name":"Me myself","nicknames":["aaa", "bbb"]}); -``` - -If you use golang JSON structs as responses from your API, you may want to have a common prefix for all the generated models: - -```golang -converter := typescriptify.New(). -converter.Prefix = "API_" -converter.Add(Person{}) -``` - -The model name will be `API_Person` instead of `Person`. - -## Field comments - -Field documentation comments can be added with the `ts_doc` tag: - -```golang -type Person struct { - Name string `json:"name" ts_doc:"This is a comment"` -} -``` - -Generated typescript: - -```typescript -export class Person { - /** This is a comment */ - name: string; -} -``` - -## Custom types - -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: - -```golang -type Data struct { - Counters map[string]int `json:"counters" ts_type:"CustomType"` -} -``` - -...will create: - -```typescript -export class Data { - counters: CustomType; -} -``` - -If the JSON field needs some special handling before converting it to a javascript object, use `ts_transform`. -For example: - -```golang -type Data struct { - Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"` -} -``` - -Generated typescript: - -```typescript -export class Date { - time: Date; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.time = new Date(source["time"]); - } -} -``` - -In this case, you should always use `new Data(json)` instead of just casting `json`. - -If you use a custom type that has to be imported, you can do the following: - -```golang -converter := typescriptify.New() -converter.AddImport("import Decimal from 'decimal.js'") -``` - -This will put your import on top of the generated file. - -## Global custom types - -Additionally, you can tell the library to automatically use a given Typescript type and custom transformation for a type: - -```golang -converter := New() -converter.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"}) -``` - -If you only want to change `ts_transform` but not `ts_type`, you can pass an empty string. - -## Enums - -There are two ways to create enums. - -### Enums with TSName() - -In this case you must provide a list of enum values and the enum type must have a `TSName() string` method - -```golang -type Weekday int - -const ( - Sunday Weekday = iota - Monday - Tuesday - Wednesday - Thursday - Friday - Saturday -) - -var AllWeekdays = []Weekday{ Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, } - -func (w Weekday) TSName() string { - switch w { - case Sunday: - return "SUNDAY" - case Monday: - return "MONDAY" - case Tuesday: - return "TUESDAY" - case Wednesday: - return "WEDNESDAY" - case Thursday: - return "THURSDAY" - case Friday: - return "FRIDAY" - case Saturday: - return "SATURDAY" - default: - return "???" - } -} -``` - -If this is too verbose for you, you can also provide a list of enums and enum names: - -```golang -var AllWeekdays = []struct { - Value Weekday - TSName string -}{ - {Sunday, "SUNDAY"}, - {Monday, "MONDAY"}, - {Tuesday, "TUESDAY"}, - {Wednesday, "WEDNESDAY"}, - {Thursday, "THURSDAY"}, - {Friday, "FRIDAY"}, - {Saturday, "SATURDAY"}, -} -``` - -Then, when converting models `AddEnum()` to specify the enum: - -```golang - converter := New(). - AddEnum(AllWeekdays) -``` - -The resulting code will be: - -```typescript -export enum Weekday { - SUNDAY = 0, - MONDAY = 1, - TUESDAY = 2, - WEDNESDAY = 3, - THURSDAY = 4, - FRIDAY = 5, - SATURDAY = 6, -} -export class Holliday { - name: string; - weekday: Weekday; -} -``` - -## License - -This library is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) - +# A Golang JSON to TypeScript model converter + +## Installation + +The command-line tool: + +``` +go get github.com/GoodNotes/typescriptify-golang-structs/tscriptify +``` + +The library: + +``` +go get github.com/GoodNotes/typescriptify-golang-structs +``` + +## Usage + +Use the command line tool: + +``` +tscriptify -package=package/with/your/models -target=target_ts_file.ts Model1 Model2 +``` + +If you need to import a custom type in Typescript, you can pass the import string: + +``` +tscriptify -package=package/with/your/models -target=target_ts_file.ts -import="import { Decimal } from 'decimal.js'" Model1 Model2 +``` + +If all your structs are in one file, you can convert them with: + +``` +tscriptify -package=package/with/your/models -target=target_ts_file.ts path/to/file/with/structs.go +``` + +Or by using it from your code: + +```golang +converter := typescriptify.New(). + Add(Person{}). + Add(Dummy{}) +err := converter.ConvertToFile("ts/models.ts") +if err != nil { + panic(err.Error()) +} +``` + +Command line options: + +``` +$ tscriptify --help +Usage of tscriptify: + -all-optional + Create interfaces with all fields optional + -backup string + Directory where backup files are saved + -import value + Typescript import for your custom type, repeat this option for each import needed + -interface + Create interfaces (not classes) + -package string + Path of the package with models + -readonly + Create interfaces with readonly fields + -target string + Target typescript file + -verbose + Verbose logs +``` + +## Models and conversion + +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. + +Example input structs: + +```golang +type Address struct { + City string `json:"city"` + Number float64 `json:"number"` + Country string `json:"country,omitempty"` +} + +type PersonalInfo struct { + Hobbies []string `json:"hobby"` + PetName string `json:"pet_name"` +} + +type Person struct { + Name string `json:"name"` + PersonalInfo PersonalInfo `json:"personal_info"` + Nicknames []string `json:"nicknames"` + Addresses []Address `json:"addresses"` + Address *Address `json:"address"` + Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"` + Friends []*Person `json:"friends"` +} +``` + +Generated TypeScript: + +```typescript +export class Address { + city: string; + number: number; + country?: string; + + constructor(source: any = {}) { + if ("string" === typeof source) source = JSON.parse(source); + this.city = source["city"]; + this.number = source["number"]; + this.country = source["country"]; + } +} +export class PersonalInfo { + hobby: string[]; + pet_name: string; + + constructor(source: any = {}) { + if ("string" === typeof source) source = JSON.parse(source); + this.hobby = source["hobby"]; + this.pet_name = source["pet_name"]; + } +} +export class Person { + name: string; + personal_info: PersonalInfo; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: { [key: string]: string }; + friends: Person[]; + + constructor(source: any = {}) { + if ("string" === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.personal_info = this.convertValues( + source["personal_info"], + PersonalInfo + ); + this.nicknames = source["nicknames"]; + this.addresses = this.convertValues(source["addresses"], Address); + this.address = this.convertValues(source["address"], Address); + this.metadata = source["metadata"]; + this.friends = this.convertValues(source["friends"], Person); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice) { + return (a as any[]).map((elem) => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +``` + +If you prefer interfaces, the output is: + +```typescript +export interface Address { + city: string; + number: number; + country?: string; +} +export interface PersonalInfo { + hobby: string[]; + pet_name: string; +} +export interface Person { + name: string; + personal_info: PersonalInfo; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: { [key: string]: string }; + friends: Person[]; +} +``` + +In TypeScript you can just cast your json object in any of those models: + +```typescript +var person = { name: "Me myself", nicknames: ["aaa", "bbb"] }; +console.log(person.name); +// The TypeScript compiler will throw an error for this line +console.log(person.something); +``` + +## Custom Typescript code + +Any custom code can be added to Typescript models: + +```typescript +class Address { + street: string; + no: number; + //[Address:] + country: string; + getStreetAndNumber() { + return street + " " + number; + } + //[end] +} +``` + +The lines between `//[Address:]` and `//[end]` will be left intact after `ConvertToFile()`. + +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. + +In that case use the constructor: + +```typescript +var person = new Person({ name: "Me myself", nicknames: ["aaa", "bbb"] }); +``` + +If you use golang JSON structs as responses from your API, you may want to have a common prefix for all the generated models: + +```golang +converter := typescriptify.New(). +converter.Prefix = "API_" +converter.Add(Person{}) +``` + +The model name will be `API_Person` instead of `Person`. + +## Field comments + +Field documentation comments can be added with the `ts_doc` tag: + +```golang +type Person struct { + Name string `json:"name" ts_doc:"This is a comment"` +} +``` + +Generated typescript: + +```typescript +export class Person { + /** This is a comment */ + name: string; +} +``` + +## Custom types + +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: + +```golang +type Data struct { + Counters map[string]int `json:"counters" ts_type:"CustomType"` +} +``` + +...will create: + +```typescript +export class Data { + counters: CustomType; +} +``` + +If the JSON field needs some special handling before converting it to a javascript object, use `ts_transform`. +For example: + +```golang +type Data struct { + Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"` +} +``` + +Generated typescript: + +```typescript +export class Date { + time: Date; + + constructor(source: any = {}) { + if ("string" === typeof source) source = JSON.parse(source); + this.time = new Date(source["time"]); + } +} +``` + +In this case, you should always use `new Data(json)` instead of just casting `json`. + +If you use a custom type that has to be imported, you can do the following: + +```golang +converter := typescriptify.New() +converter.AddImport("import Decimal from 'decimal.js'") +``` + +This will put your import on top of the generated file. + +## Global custom types + +Additionally, you can tell the library to automatically use a given Typescript type and custom transformation for a type: + +```golang +converter := New() +converter.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"}) +``` + +If you only want to change `ts_transform` but not `ts_type`, you can pass an empty string. + +## Enums + +There are two ways to create enums. + +### Enums with TSName() + +In this case you must provide a list of enum values and the enum type must have a `TSName() string` method + +```golang +type Weekday int + +const ( + Sunday Weekday = iota + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday +) + +var AllWeekdays = []Weekday{ Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, } + +func (w Weekday) TSName() string { + switch w { + case Sunday: + return "SUNDAY" + case Monday: + return "MONDAY" + case Tuesday: + return "TUESDAY" + case Wednesday: + return "WEDNESDAY" + case Thursday: + return "THURSDAY" + case Friday: + return "FRIDAY" + case Saturday: + return "SATURDAY" + default: + return "???" + } +} +``` + +If this is too verbose for you, you can also provide a list of enums and enum names: + +```golang +var AllWeekdays = []struct { + Value Weekday + TSName string +}{ + {Sunday, "SUNDAY"}, + {Monday, "MONDAY"}, + {Tuesday, "TUESDAY"}, + {Wednesday, "WEDNESDAY"}, + {Thursday, "THURSDAY"}, + {Friday, "FRIDAY"}, + {Saturday, "SATURDAY"}, +} +``` + +Then, when converting models `AddEnum()` to specify the enum: + +```golang + converter := New(). + AddEnum(AllWeekdays) +``` + +The resulting code will be: + +```typescript +export enum Weekday { + SUNDAY = 0, + MONDAY = 1, + TUESDAY = 2, + WEDNESDAY = 3, + THURSDAY = 4, + FRIDAY = 5, + SATURDAY = 6, +} +export class Holliday { + name: string; + weekday: Weekday; +} +``` + +## Upstream Golang structs + +When working with upstream Golang structs which you can not directly modify, the `TagAll()` and `AddFieldTags()` methods can be used to add tags to all fields or only specific fields. + +In the below example, the AWS CSI Driver SecretDescriptor struct is converted to a [JSII](https://aws.github.io/jsii/) compatible TypeScript interface (readonly fields) as well as marking all fields as `Optional`. + +```golang +package main + +import ( + "fmt" + "reflect" + + "github.com/aws/secrets-store-csi-driver-provider-aws/provider" + "github.com/GoodNotes/typescriptify-golang-structs/typescriptify" +) + +func main() { + sdType := reflect.TypeOf(provider.SecretDescriptor{}) + sdTypeOptional := typescriptify.TagAll(sdType, []string{"omitempty"}) + + t := typescriptify. + New(). + WithReadonlyFields(true). + WithInterface(true). + WithBackupDir(""). + AddTypeWithName(sdTypeOptional, "SecretDescriptor") + + err := t.ConvertToFile("./driver-provider-aws.secrets-store.csi.x-k8s.io.generated.ts") + if err != nil { + panic(err.Error()) + } + fmt.Println("OK") +} +``` + +Below snippet shows how to set the field `ObjectType` of the above `SecretDescriptor` struct to a Union type of literal Types using the `AddFieldTags` method: + +```golang + sdType := reflect.TypeOf(provider.SecretDescriptor{}) + // set sd ObjectType to "ssmparameter" | "secretsmanager" + fieldTags := make(FieldTags) + fieldTags["ObjectType"] = []*structtag.Tag{ + { + Key: "ts_type", + Name: "\"ssmparameter\" | \"secretsmanager\"", + Options: []string{}, + }, + } + sdTypeTagged := typescriptify.AddFieldTags(sdType, &fieldTags) + + t := typescriptify. + New(). + AddTypeWithName(sdTypeTagged, "SecretDescriptor") +``` + +> Note: In both of these cases use the `AddTypeWithName` method to explicitly provide the name for the generated TypeScript interface. +> By design `reflect.Type` returns an empty string for non-defined types. +> +> Ref: https://forum.golangbridge.org/t/is-it-possible-to-give-type-name-to-dynamic-creation-struct-from-reflect-structof/18198/4 + +## License + +This library is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/example/example.go b/example/example.go index ec8e4cb..d562886 100644 --- a/example/example.go +++ b/example/example.go @@ -1,45 +1,45 @@ -package main - -import "github.com/tkrajina/typescriptify-golang-structs/typescriptify" - -type Address struct { - // Used in html - City string `json:"city"` - Number float64 `json:"number"` - Country string `json:"country,omitempty"` -} - -type PersonalInfo struct { - Hobbies []string `json:"hobby"` - PetName string `json:"pet_name"` -} - -type Person struct { - Name string `json:"name"` - PersonalInfo PersonalInfo `json:"personal_info"` - Nicknames []string `json:"nicknames"` - Addresses []Address `json:"addresses"` - Address *Address `json:"address"` - Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"` - Friends []*Person `json:"friends"` -} - -func main() { - converter := typescriptify.New() - converter.CreateConstructor = true - converter.Indent = " " - converter.BackupDir = "" - - converter.Add(Person{}) - - err := converter.ConvertToFile("browser_test/example_output.ts") - if err != nil { - panic(err.Error()) - } - - converter.CreateInterface = true - err = converter.ConvertToFile("browser_test/example_output_interfaces.ts") - if err != nil { - panic(err.Error()) - } -} +package main + +import "github.com/GoodNotes/typescriptify-golang-structs/typescriptify" + +type Address struct { + // Used in html + City string `json:"city"` + Number float64 `json:"number"` + Country string `json:"country,omitempty"` +} + +type PersonalInfo struct { + Hobbies []string `json:"hobby"` + PetName string `json:"pet_name"` +} + +type Person struct { + Name string `json:"name"` + PersonalInfo PersonalInfo `json:"personal_info"` + Nicknames []string `json:"nicknames"` + Addresses []Address `json:"addresses"` + Address *Address `json:"address"` + Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"` + Friends []*Person `json:"friends"` +} + +func main() { + converter := typescriptify.New() + converter.CreateConstructor = true + converter.Indent = " " + converter.BackupDir = "" + + converter.Add(Person{}) + + err := converter.ConvertToFile("browser_test/example_output.ts") + if err != nil { + panic(err.Error()) + } + + converter.CreateInterface = true + err = converter.ConvertToFile("browser_test/example_output_interfaces.ts") + if err != nil { + panic(err.Error()) + } +} diff --git a/go.mod b/go.mod index 540730c..619693a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,16 @@ -module github.com/tkrajina/typescriptify-golang-structs +module github.com/GoodNotes/typescriptify-golang-structs -go 1.16 +go 1.21 require ( + github.com/fatih/structtag v1.2.0 github.com/stretchr/testify v1.7.0 github.com/tkrajina/go-reflector v0.5.5 + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 57bc8a7..89b4196 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,17 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tkrajina/go-reflector v0.5.4 h1:dS9aJEa/eYNQU/fwsb5CSiATOxcNyA/gG/A7a582D5s= -github.com/tkrajina/go-reflector v0.5.4/go.mod h1:9PyLgEOzc78ey/JmQQHbW8cQJ1oucLlNQsg8yFvkVk8= github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/makefile b/makefile index 5437b3e..2ec24b3 100644 --- a/makefile +++ b/makefile @@ -1,21 +1,25 @@ -.PHONY: build -build: - go build -i -v -o /dev/null ./... - -.PHONY: install -install: - go install ./... - -.PHONY: test -test: lint - go test ./... - go run example/example.go - tsc browser_test/example_output.ts - # Make sure dommandline tool works: - 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 - 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 - -.PHONY: lint -lint: - go vet ./... - -golangci-lint run +.PHONY: build +build: + go build -v -o /dev/null ./... + +.PHONY: install +install: + go install ./... + +.PHONY: test +test: node_modules lint + go test ./... + go run example/example.go + npx tsc browser_test/example_output.ts + # Make sure dommandline tool works: + go run tscriptify/main.go -package github.com/GoodNotes/typescriptify-golang-structs/example/example-models -verbose -target tmp_classes.ts example/example-models/example_models.go + go run tscriptify/main.go -package github.com/GoodNotes/typescriptify-golang-structs/example/example-models -verbose -target tmp_interfaces.ts -interface example/example-models/example_models.go + go run tscriptify/main.go -package=github.com/aws/secrets-store-csi-driver-provider-aws/provider -verbose -target=tmp_jsiiIntefaces.ts -interface -readonly -all-optional SecretDescriptor + +.PHONY: lint +lint: + go vet ./... + -golangci-lint run + +node_modules: + npm install diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ebd19ea --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "typescriptify-golang-structs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typescriptify-golang-structs", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "typescript": "^5.3.3" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1c14dd5 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "typescriptify-golang-structs", + "version": "1.0.0", + "description": "The command-line tool:", + "main": "index.js", + "directories": { + "example": "example" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^5.3.3" + } +} diff --git a/tscriptify/main.go b/tscriptify/main.go index 4f8d939..18e3b9a 100644 --- a/tscriptify/main.go +++ b/tscriptify/main.go @@ -1,172 +1,218 @@ -package main - -import ( - "flag" - "fmt" - "go/ast" - "go/parser" - "go/token" - "os" - "os/exec" - "strings" - "text/template" -) - -type arrayImports []string - -func (i *arrayImports) String() string { - return "// custom imports:\n\n" + strings.Join(*i, "\n") -} - -func (i *arrayImports) Set(value string) error { - *i = append(*i, value) - return nil -} - -const TEMPLATE = `package main - -import ( - "fmt" - - m "{{ .ModelsPackage }}" - "github.com/tkrajina/typescriptify-golang-structs/typescriptify" -) - -func main() { - t := typescriptify.New() - t.CreateInterface = {{ .Interface }} -{{ range $key, $value := .InitParams }} t.{{ $key }}={{ $value }} -{{ end }} -{{ range .Structs }} t.Add({{ . }}{}) -{{ end }} -{{ range .CustomImports }} t.AddImport("{{ . }}") -{{ end }} - err := t.ConvertToFile("{{ .TargetFile }}") - if err != nil { - panic(err.Error()) - } - fmt.Println("OK") -}` - -type Params struct { - ModelsPackage string - TargetFile string - Structs []string - InitParams map[string]interface{} - CustomImports arrayImports - Interface bool - Verbose bool -} - -func main() { - var p Params - var backupDir string - flag.StringVar(&p.ModelsPackage, "package", "", "Path of the package with models") - flag.StringVar(&p.TargetFile, "target", "", "Target typescript file") - flag.StringVar(&backupDir, "backup", "", "Directory where backup files are saved") - flag.BoolVar(&p.Interface, "interface", false, "Create interfaces (not classes)") - flag.Var(&p.CustomImports, "import", "Typescript import for your custom type, repeat this option for each import needed") - flag.BoolVar(&p.Verbose, "verbose", false, "Verbose logs") - flag.Parse() - - structs := []string{} - for _, structOrGoFile := range flag.Args() { - if strings.HasSuffix(structOrGoFile, ".go") { - fmt.Println("Parsing:", structOrGoFile) - fileStructs, err := GetGolangFileStructs(structOrGoFile) - if err != nil { - panic(fmt.Sprintf("Error loading/parsing golang file %s: %s", structOrGoFile, err.Error())) - } - structs = append(structs, fileStructs...) - } else { - structs = append(structs, structOrGoFile) - } - } - - if len(p.ModelsPackage) == 0 { - fmt.Fprintln(os.Stderr, "No package given") - os.Exit(1) - } - if len(p.TargetFile) == 0 { - fmt.Fprintln(os.Stderr, "No target file") - os.Exit(1) - } - - t := template.Must(template.New("").Parse(TEMPLATE)) - - f, err := os.CreateTemp(os.TempDir(), "typescriptify_*.go") - handleErr(err) - defer f.Close() - - structsArr := make([]string, 0) - for _, str := range structs { - str = strings.TrimSpace(str) - if len(str) > 0 { - structsArr = append(structsArr, "m."+str) - } - } - - p.Structs = structsArr - p.InitParams = map[string]interface{}{ - "BackupDir": fmt.Sprintf(`"%s"`, backupDir), - } - err = t.Execute(f, p) - handleErr(err) - - if p.Verbose { - byts, err := os.ReadFile(f.Name()) - handleErr(err) - fmt.Printf("\nCompiling generated code (%s):\n%s\n----------------------------------------------------------------------------------------------------\n", f.Name(), string(byts)) - } - - cmd := exec.Command("go", "run", f.Name()) - fmt.Println(strings.Join(cmd.Args, " ")) - output, err := cmd.CombinedOutput() - if err != nil { - fmt.Println(string(output)) - handleErr(err) - } - fmt.Println(string(output)) -} - -func GetGolangFileStructs(filename string) ([]string, error) { - fset := token.NewFileSet() // positions are relative to fset - - f, err := parser.ParseFile(fset, filename, nil, 0) - if err != nil { - return nil, err - } - - v := &AVisitor{} - ast.Walk(v, f) - - return v.structs, nil -} - -type AVisitor struct { - structNameCandidate string - structs []string -} - -func (v *AVisitor) Visit(node ast.Node) ast.Visitor { - if node != nil { - switch t := node.(type) { - case *ast.Ident: - v.structNameCandidate = t.Name - case *ast.StructType: - if len(v.structNameCandidate) > 0 { - v.structs = append(v.structs, v.structNameCandidate) - v.structNameCandidate = "" - } - default: - v.structNameCandidate = "" - } - } - return v -} - -func handleErr(err error) { - if err != nil { - panic(err.Error()) - } -} +package main + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" +) + +type arrayImports []string + +func (i *arrayImports) String() string { + return "// custom imports:\n\n" + strings.Join(*i, "\n") +} + +func (i *arrayImports) Set(value string) error { + *i = append(*i, value) + return nil +} + +const TEMPLATE = `package main + +import ( + "fmt" +{{- if .AllOptional }} + "reflect" +{{- end }} + + m "{{ .ModelsPackage }}" + "github.com/GoodNotes/typescriptify-golang-structs/typescriptify" +) + +func main() { +{{ if .AllOptional }} +{{ range .Structs }} {{ . }}Optional := typescriptify.TagAll(reflect.TypeOf(m.{{ . }}{}), []string{"omitempty"}) +{{ end }} +{{ end }} + t := typescriptify.New() + t.CreateInterface = {{ .Interface }} + t.ReadOnlyFields = {{ .Readonly }} +{{ range $key, $value := .InitParams }} t.{{ $key }}={{ $value }} +{{ end }} +{{ if .AllOptional }} +{{ range .Structs }} t.AddTypeWithName({{ . }}Optional, "{{ . }}") +{{ end }} +{{ else }} +{{ range .Structs }} t.Add(m.{{ . }}{}) +{{ end }} +{{ end }} +{{ range .CustomImports }} t.AddImport("{{ . }}") +{{ end }} + err := t.ConvertToFile("{{ .TargetFile }}") + if err != nil { + panic(err.Error()) + } + fmt.Println("OK") +}` + +type Params struct { + ModelsPackage string + TargetFile string + Structs []string + InitParams map[string]interface{} + CustomImports arrayImports + Interface bool + Readonly bool + AllOptional bool + Verbose bool +} + +func main() { + var p Params + var backupDir string + flag.StringVar(&p.ModelsPackage, "package", "", "Path of the package with models") + flag.StringVar(&p.TargetFile, "target", "", "Target typescript file") + flag.StringVar(&backupDir, "backup", "", "Directory where backup files are saved") + flag.BoolVar(&p.Interface, "interface", false, "Create interfaces (not classes)") + flag.BoolVar(&p.Readonly, "readonly", false, "Create interfaces with readonly fields") + flag.BoolVar(&p.AllOptional, "all-optional", false, "Create interfaces with all fields optional") + flag.Var(&p.CustomImports, "import", "Typescript import for your custom type, repeat this option for each import needed") + flag.BoolVar(&p.Verbose, "verbose", false, "Verbose logs") + flag.Parse() + + structs := []string{} + for _, structOrGoFile := range flag.Args() { + if strings.HasSuffix(structOrGoFile, ".go") { + fmt.Println("Parsing:", structOrGoFile) + fileStructs, err := GetGolangFileStructs(structOrGoFile) + if err != nil { + panic(fmt.Sprintf("Error loading/parsing golang file %s: %s", structOrGoFile, err.Error())) + } + structs = append(structs, fileStructs...) + } else { + structs = append(structs, structOrGoFile) + } + } + + if len(p.ModelsPackage) == 0 { + fmt.Fprintln(os.Stderr, "No package given") + os.Exit(1) + } + if len(p.TargetFile) == 0 { + fmt.Fprintln(os.Stderr, "No target file") + os.Exit(1) + } + + t := template.Must(template.New("").Parse(TEMPLATE)) + + d, err := os.MkdirTemp("", "tscriptify") + handleErr(err) + defer os.RemoveAll(d) + + f, err := os.CreateTemp(d, "main*.go") + handleErr(err) + defer f.Close() + + structsArr := make([]string, 0) + for _, str := range structs { + str = strings.TrimSpace(str) + if len(str) > 0 { + structsArr = append(structsArr, str) + } + } + + p.Structs = structsArr + p.InitParams = map[string]interface{}{ + "BackupDir": fmt.Sprintf(`"%s"`, backupDir), + } + err = t.Execute(f, p) + handleErr(err) + + if p.Verbose { + byts, err := os.ReadFile(f.Name()) + handleErr(err) + fmt.Printf("\nCompiling generated code (%s):\n%s\n----------------------------------------------------------------------------------------------------\n", f.Name(), string(byts)) + } + + var cmd *exec.Cmd + cmdInit := exec.Command("go", "mod", "init", "tmp") + fmt.Println(d + ": " + strings.Join(cmdInit.Args, " ")) + cmdInit.Dir = d + initOutput, err := cmdInit.CombinedOutput() + if err != nil { + fmt.Println(string(initOutput)) + handleErr(err) + } + fmt.Println(string(initOutput)) + cmdGet := exec.Command("go", "get", "-v") + cmdGet.Env = append(os.Environ(), "GO111MODULE=on") + fmt.Println(d + ": " + strings.Join(cmdGet.Args, " ")) + cmdGet.Dir = d + getOutput, err := cmdGet.CombinedOutput() + if err != nil { + fmt.Println(string(getOutput)) + handleErr(err) + } + fmt.Println(string(getOutput)) + cmd = exec.Command("go", "run", ".") + cmd.Dir = d + fmt.Println(d + ": " + strings.Join(cmd.Args, " ")) + + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(output)) + handleErr(err) + } + fmt.Println(string(output)) + err = os.Rename(filepath.Join(d, p.TargetFile), p.TargetFile) + handleErr(err) +} + +func GetGolangFileStructs(filename string) ([]string, error) { + fset := token.NewFileSet() // positions are relative to fset + + f, err := parser.ParseFile(fset, filename, nil, 0) + if err != nil { + return nil, err + } + + v := &AVisitor{} + ast.Walk(v, f) + + return v.structs, nil +} + +type AVisitor struct { + structNameCandidate string + structs []string +} + +func (v *AVisitor) Visit(node ast.Node) ast.Visitor { + if node != nil { + switch t := node.(type) { + case *ast.Ident: + v.structNameCandidate = t.Name + case *ast.StructType: + if len(v.structNameCandidate) > 0 { + v.structs = append(v.structs, v.structNameCandidate) + v.structNameCandidate = "" + } + default: + v.structNameCandidate = "" + } + } + return v +} + +func handleErr(err error) { + if err != nil { + panic(err.Error()) + } +} diff --git a/typescriptify/typescriptify.go b/typescriptify/typescriptify.go index fbb71b2..a759b4a 100644 --- a/typescriptify/typescriptify.go +++ b/typescriptify/typescriptify.go @@ -1,814 +1,906 @@ -package typescriptify - -import ( - "fmt" - "io" - "os" - "path" - "reflect" - "strings" - "time" - - "github.com/tkrajina/go-reflector/reflector" -) - -const ( - tsDocTag = "ts_doc" - tsTransformTag = "ts_transform" - tsType = "ts_type" - tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any { - if (!a) { - return a; - } - if (a.slice) { - return (a as any[]).map(elem => this.convertValues(elem, classs)); - } else if ("object" === typeof a) { - if (asMap) { - for (const key of Object.keys(a)) { - a[key] = new classs(a[key]); - } - return a; - } - return new classs(a); - } - return a; -}` -) - -// TypeOptions overrides options set by `ts_*` tags. -type TypeOptions struct { - TSType string - TSDoc string - TSTransform string -} - -// StructType stores settings for transforming one Golang struct. -type StructType struct { - Type reflect.Type - FieldOptions map[reflect.Type]TypeOptions -} - -func NewStruct(i interface{}) *StructType { - return &StructType{ - Type: reflect.TypeOf(i), - } -} - -func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType { - if st.FieldOptions == nil { - st.FieldOptions = map[reflect.Type]TypeOptions{} - } - var typ reflect.Type - if ty, is := i.(reflect.Type); is { - typ = ty - } else { - typ = reflect.TypeOf(i) - } - st.FieldOptions[typ] = opts - return st -} - -type EnumType struct { - Type reflect.Type -} - -type enumElement struct { - value interface{} - name string -} - -type TypeScriptify struct { - Prefix string - Suffix string - Indent string - CreateFromMethod bool - CreateConstructor bool - BackupDir string // If empty no backup - DontExport bool - CreateInterface bool - customImports []string - - structTypes []StructType - enumTypes []EnumType - enums map[reflect.Type][]enumElement - kinds map[reflect.Kind]string - - fieldTypeOptions map[reflect.Type]TypeOptions - - // throwaway, used when converting - alreadyConverted map[reflect.Type]bool -} - -func New() *TypeScriptify { - result := new(TypeScriptify) - result.Indent = "\t" - result.BackupDir = "." - - kinds := make(map[reflect.Kind]string) - - kinds[reflect.Bool] = "boolean" - kinds[reflect.Interface] = "any" - - kinds[reflect.Int] = "number" - kinds[reflect.Int8] = "number" - kinds[reflect.Int16] = "number" - kinds[reflect.Int32] = "number" - kinds[reflect.Int64] = "number" - kinds[reflect.Uint] = "number" - kinds[reflect.Uint8] = "number" - kinds[reflect.Uint16] = "number" - kinds[reflect.Uint32] = "number" - kinds[reflect.Uint64] = "number" - kinds[reflect.Float32] = "number" - kinds[reflect.Float64] = "number" - - kinds[reflect.String] = "string" - - result.kinds = kinds - - result.Indent = " " - result.CreateFromMethod = false - result.CreateConstructor = true - - return result -} - -func deepFields(typeOf reflect.Type) []reflect.StructField { - fields := make([]reflect.StructField, 0) - - if typeOf.Kind() == reflect.Ptr { - typeOf = typeOf.Elem() - } - - if typeOf.Kind() != reflect.Struct { - return fields - } - - for i := 0; i < typeOf.NumField(); i++ { - f := typeOf.Field(i) - - kind := f.Type.Kind() - if f.Anonymous && kind == reflect.Struct { - //fmt.Println(v.Interface()) - fields = append(fields, deepFields(f.Type)...) - } else if f.Anonymous && kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct { - //fmt.Println(v.Interface()) - fields = append(fields, deepFields(f.Type.Elem())...) - } else { - fields = append(fields, f) - } - } - - return fields -} - -func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) { - fmt.Printf(strings.Repeat(" ", depth)+s+"\n", args...) -} - -// ManageType can define custom options for fields of a specified type. -// -// This can be used instead of setting ts_type and ts_transform for all fields of a certain type. -func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify { - var typ reflect.Type - switch t := fld.(type) { - case reflect.Type: - typ = t - default: - typ = reflect.TypeOf(fld) - } - if t.fieldTypeOptions == nil { - t.fieldTypeOptions = map[reflect.Type]TypeOptions{} - } - t.fieldTypeOptions[typ] = opts - return t -} - -func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify { - t.CreateFromMethod = b - return t -} - -func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify { - t.CreateInterface = b - return t -} - -func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify { - t.CreateConstructor = b - return t -} - -func (t *TypeScriptify) WithIndent(i string) *TypeScriptify { - t.Indent = i - return t -} - -func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify { - t.BackupDir = b - return t -} - -func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify { - t.Prefix = p - return t -} - -func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify { - t.Suffix = s - return t -} - -func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify { - switch ty := obj.(type) { - case StructType: - t.structTypes = append(t.structTypes, ty) - case *StructType: - t.structTypes = append(t.structTypes, *ty) - case reflect.Type: - t.AddType(ty) - default: - t.AddType(reflect.TypeOf(obj)) - } - return t -} - -func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify { - t.structTypes = append(t.structTypes, StructType{Type: typeOf}) - return t -} - -func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) { - keyType := field.Type.Key() - valueType := field.Type.Elem() - valueTypeName := valueType.Name() - if name, ok := t.types[valueType.Kind()]; ok { - valueTypeName = name - } - if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice { - valueTypeName = valueType.Elem().Name() + "[]" - } - if valueType.Kind() == reflect.Ptr { - valueTypeName = valueType.Elem().Name() - } - strippedFieldName := strings.ReplaceAll(fieldName, "?", "") - - keyTypeStr := keyType.Name() - // Key should always be string, no need for this: - // _, isSimple := t.types[keyType.Kind()] - // if !isSimple { - // keyTypeStr = t.prefix + keyType.Name() + t.suffix - // } - - if valueType.Kind() == reflect.Struct { - t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, t.prefix+valueTypeName)) - 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)) - } else { - t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, valueTypeName)) - t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = source[\"%s\"];", t.indent, t.indent, strippedFieldName, strippedFieldName)) - } -} - -func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify { - if t.enums == nil { - t.enums = map[reflect.Type][]enumElement{} - } - items := reflect.ValueOf(values) - if items.Kind() != reflect.Slice { - panic(fmt.Sprintf("Values for %T isn't a slice", values)) - } - - var elements []enumElement - for i := 0; i < items.Len(); i++ { - item := items.Index(i) - - var el enumElement - if item.Kind() == reflect.Struct { - r := reflector.New(item.Interface()) - val, err := r.Field("Value").Get() - if err != nil { - panic(fmt.Sprint("missing Type field in ", item.Type().String())) - } - name, err := r.Field("TSName").Get() - if err != nil { - panic(fmt.Sprint("missing TSName field in ", item.Type().String())) - } - el.value = val - el.name = name.(string) - } else { - el.value = item.Interface() - if tsNamer, is := item.Interface().(TSNamer); is { - el.name = tsNamer.TSName() - } else { - panic(fmt.Sprint(item.Type().String(), " has no TSName method")) - } - } - - elements = append(elements, el) - } - ty := reflect.TypeOf(elements[0].value) - t.enums[ty] = elements - t.enumTypes = append(t.enumTypes, EnumType{Type: ty}) - - return t -} - -// AddEnumValues is deprecated, use `AddEnum()` -func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify { - t.AddEnum(values) - return t -} - -func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) { - if t.CreateFromMethod { - fmt.Fprintln(os.Stderr, "FromMethod METHOD IS DEPRECATED AND WILL BE REMOVED!!!!!!") - } - - t.alreadyConverted = make(map[reflect.Type]bool) - depth := 0 - - result := "" - if len(t.customImports) > 0 { - // Put the custom imports, i.e.: `import Decimal from 'decimal.js'` - for _, cimport := range t.customImports { - result += cimport + "\n" - } - } - - for _, enumTyp := range t.enumTypes { - elements := t.enums[enumTyp.Type] - typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements) - if err != nil { - return "", err - } - result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") - } - - for _, strctTyp := range t.structTypes { - typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode) - if err != nil { - return "", err - } - result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") - } - return result, nil -} - -func loadCustomCode(fileName string) (map[string]string, error) { - result := make(map[string]string) - f, err := os.Open(fileName) - if err != nil { - if os.IsNotExist(err) { - return result, nil - } - return result, err - } - defer f.Close() - - bytes, err := io.ReadAll(f) - if err != nil { - return result, err - } - - var currentName string - var currentValue string - lines := strings.Split(string(bytes), "\n") - for _, line := range lines { - trimmedLine := strings.TrimSpace(line) - if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") { - currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1) - currentValue = "" - } else if trimmedLine == "//[end]" { - result[currentName] = strings.TrimRight(currentValue, " \t\r\n") - currentName = "" - currentValue = "" - } else if len(currentName) > 0 { - currentValue += line + "\n" - } - } - - return result, nil -} - -func (t TypeScriptify) backup(fileName string) error { - fileIn, err := os.Open(fileName) - if err != nil { - if !os.IsNotExist(err) { - return err - } - // No neet to backup, just return: - return nil - } - defer fileIn.Close() - - bytes, err := io.ReadAll(fileIn) - if err != nil { - return err - } - - _, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99"))) - if t.BackupDir != "" { - backupFn = path.Join(t.BackupDir, backupFn) - } - - return os.WriteFile(backupFn, bytes, os.FileMode(0700)) -} - -func (t TypeScriptify) ConvertToFile(fileName string) error { - if len(t.BackupDir) > 0 { - err := t.backup(fileName) - if err != nil { - return err - } - } - - customCode, err := loadCustomCode(fileName) - if err != nil { - return err - } - - f, err := os.Create(fileName) - if err != nil { - return err - } - defer f.Close() - - converted, err := t.Convert(customCode) - if err != nil { - return err - } - - if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil { - return err - } - if _, err := f.WriteString(converted); err != nil { - return err - } - if err != nil { - return err - } - - return nil -} - -type TSNamer interface { - TSName() string -} - -func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) { - t.logf(depth, "Converting enum %s", typeOf.String()) - if _, found := t.alreadyConverted[typeOf]; found { // Already converted - return "", nil - } - t.alreadyConverted[typeOf] = true - - entityName := t.Prefix + typeOf.Name() + t.Suffix - result := "enum " + entityName + " {\n" - - for _, val := range elements { - result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value) - } - - result += "}" - - if !t.DontExport { - result = "export " + result - } - - return result, nil -} - -func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions { - // By default use options defined by tags: - opts := TypeOptions{ - TSTransform: field.Tag.Get(tsTransformTag), - TSType: field.Tag.Get(tsType), - TSDoc: field.Tag.Get(tsDocTag), - } - - overrides := []TypeOptions{} - - // But there is maybe an struct-specific override: - for _, strct := range t.structTypes { - if strct.FieldOptions == nil { - continue - } - if strct.Type == structType { - if fldOpts, found := strct.FieldOptions[field.Type]; found { - overrides = append(overrides, fldOpts) - } - } - } - - if fldOpts, found := t.fieldTypeOptions[field.Type]; found { - overrides = append(overrides, fldOpts) - } - - for _, o := range overrides { - if o.TSTransform != "" { - opts.TSTransform = o.TSTransform - } - if o.TSType != "" { - opts.TSType = o.TSType - } - } - - return opts -} - -func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string { - jsonFieldName := "" - jsonTag := field.Tag.Get("json") - if len(jsonTag) > 0 { - jsonTagParts := strings.Split(jsonTag, ",") - if len(jsonTagParts) > 0 { - jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent) - } - hasOmitEmpty := false - ignored := false - for _, t := range jsonTagParts { - if t == "" { - break - } - if t == "omitempty" { - hasOmitEmpty = true - break - } - if t == "-" { - ignored = true - break - } - } - if !ignored && isPtr || hasOmitEmpty { - jsonFieldName = fmt.Sprintf("%s?", jsonFieldName) - } - } else if /*field.IsExported()*/ field.PkgPath == "" { - jsonFieldName = field.Name - } - return jsonFieldName -} - -func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) { - if _, found := t.alreadyConverted[typeOf]; found { // Already converted - return "", nil - } - t.logf(depth, "Converting type %s", typeOf.String()) - - t.alreadyConverted[typeOf] = true - - entityName := t.Prefix + typeOf.Name() + t.Suffix - result := "" - if t.CreateInterface { - result += fmt.Sprintf("interface %s {\n", entityName) - } else { - result += fmt.Sprintf("class %s {\n", entityName) - } - if !t.DontExport { - result = "export " + result - } - builder := typeScriptClassBuilder{ - types: t.kinds, - indent: t.Indent, - prefix: t.Prefix, - suffix: t.Suffix, - } - - fields := deepFields(typeOf) - for _, field := range fields { - isPtr := field.Type.Kind() == reflect.Ptr - if isPtr { - field.Type = field.Type.Elem() - } - jsonFieldName := t.getJSONFieldName(field, isPtr) - if len(jsonFieldName) == 0 || jsonFieldName == "-" { - continue - } - - var err error - fldOpts := t.getFieldOptions(typeOf, field) - if fldOpts.TSDoc != "" { - builder.addFieldDefinitionLine("/** " + fldOpts.TSDoc + " */") - } - if fldOpts.TSTransform != "" { - t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) - err = builder.AddSimpleField(jsonFieldName, field, fldOpts) - } else if _, isEnum := t.enums[field.Type]; isEnum { - t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name) - builder.AddEnumField(jsonFieldName, field) - } else if fldOpts.TSType != "" { // Struct: - t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) - err = builder.AddSimpleField(jsonFieldName, field, fldOpts) - } else if field.Type.Kind() == reflect.Struct { // Struct: - t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) - typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode) - if err != nil { - return "", err - } - if typeScriptChunk != "" { - result = typeScriptChunk + "\n" + result - } - builder.AddStructField(jsonFieldName, field) - } else if field.Type.Kind() == reflect.Map { - t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name) - // Also convert map key types if needed - var keyTypeToConvert reflect.Type - switch field.Type.Key().Kind() { - case reflect.Struct: - keyTypeToConvert = field.Type.Key() - case reflect.Ptr: - keyTypeToConvert = field.Type.Key().Elem() - } - if keyTypeToConvert != nil { - typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode) - if err != nil { - return "", err - } - if typeScriptChunk != "" { - result = typeScriptChunk + "\n" + result - } - } - // Also convert map value types if needed - var valueTypeToConvert reflect.Type - switch field.Type.Elem().Kind() { - case reflect.Struct: - valueTypeToConvert = field.Type.Elem() - case reflect.Ptr: - valueTypeToConvert = field.Type.Elem().Elem() - } - if valueTypeToConvert != nil { - typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode) - if err != nil { - return "", err - } - if typeScriptChunk != "" { - result = typeScriptChunk + "\n" + result - } - } - - builder.AddMapField(jsonFieldName, field) - } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice: - if field.Type.Elem().Kind() == reflect.Ptr { //extract ptr type - field.Type = field.Type.Elem() - } - - arrayDepth := 1 - for field.Type.Elem().Kind() == reflect.Slice { // Slice of slices: - field.Type = field.Type.Elem() - arrayDepth++ - } - - if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs: - t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) - typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode) - if err != nil { - return "", err - } - if typeScriptChunk != "" { - result = typeScriptChunk + "\n" + result - } - builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth) - } else { // Slice of simple fields: - t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name) - err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts) - } - } else { // Simple field: - t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) - err = builder.AddSimpleField(jsonFieldName, field, fldOpts) - } - if err != nil { - return "", err - } - } - - if t.CreateFromMethod { - t.CreateConstructor = true - } - - result += strings.Join(builder.fields, "\n") + "\n" - if !t.CreateInterface { - constructorBody := strings.Join(builder.constructorBody, "\n") - needsConvertValue := strings.Contains(constructorBody, "this.convertValues") - if t.CreateFromMethod { - result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent) - result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName) - result += fmt.Sprintf("%s}\n", t.Indent) - } - if t.CreateConstructor { - result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent) - result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n" - result += constructorBody + "\n" - result += fmt.Sprintf("%s}\n", t.Indent) - } - if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) { - result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n" - } - } - - if customCode != nil { - code := customCode[entityName] - if len(code) != 0 { - result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n" - } - } - - result += "}" - - return result, nil -} - -func (t *TypeScriptify) AddImport(i string) { - for _, cimport := range t.customImports { - if cimport == i { - return - } - } - - t.customImports = append(t.customImports, i) -} - -type typeScriptClassBuilder struct { - types map[reflect.Kind]string - indent string - fields []string - createFromMethodBody []string - constructorBody []string - prefix, suffix string -} - -func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error { - fieldType, kind := field.Type.Elem().Name(), field.Type.Elem().Kind() - typeScriptType := t.types[kind] - - if len(fieldName) > 0 { - strippedFieldName := strings.ReplaceAll(fieldName, "?", "") - if len(opts.TSType) > 0 { - t.addField(fieldName, opts.TSType) - t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) - return nil - } else if len(typeScriptType) > 0 { - t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth))) - t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) - return nil - } - } - - return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) -} - -func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error { - fieldType, kind := field.Type.Name(), field.Type.Kind() - - typeScriptType := t.types[kind] - if len(opts.TSType) > 0 { - typeScriptType = opts.TSType - } - - if len(typeScriptType) > 0 && len(fieldName) > 0 { - strippedFieldName := strings.ReplaceAll(fieldName, "?", "") - t.addField(fieldName, typeScriptType) - if opts.TSTransform == "" { - t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) - } else { - val := fmt.Sprintf(`source["%s"]`, strippedFieldName) - expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1) - t.addInitializerFieldLine(strippedFieldName, expression) - } - return nil - } - - return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) -} - -func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) { - fieldType := field.Type.Name() - t.addField(fieldName, t.prefix+fieldType+t.suffix) - strippedFieldName := strings.ReplaceAll(fieldName, "?", "") - t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) -} - -func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField) { - fieldType := field.Type.Name() - strippedFieldName := strings.ReplaceAll(fieldName, "?", "") - t.addField(fieldName, t.prefix+fieldType+t.suffix) - t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) -} - -func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) { - fieldType := field.Type.Elem().Name() - strippedFieldName := strings.ReplaceAll(fieldName, "?", "") - t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth))) - t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) -} - -func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) { - t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result.", fld, " = ", initializer, ";")) - t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this.", fld, " = ", initializer, ";")) -} - -func (t *typeScriptClassBuilder) addFieldDefinitionLine(line string) { - t.fields = append(t.fields, t.indent+line) -} - -func (t *typeScriptClassBuilder) addField(fld, fldType string) { - t.fields = append(t.fields, fmt.Sprint(t.indent, fld, ": ", fldType, ";")) -} +package typescriptify + +import ( + "fmt" + "io" + "os" + "path" + "reflect" + "strings" + "time" + + "github.com/fatih/structtag" + "github.com/tkrajina/go-reflector/reflector" + "golang.org/x/exp/slices" +) + +const ( + tsDocTag = "ts_doc" + tsTransformTag = "ts_transform" + tsType = "ts_type" + tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; +}` +) + +// TypeOptions overrides options set by `ts_*` tags. +type TypeOptions struct { + TSType string + TSDoc string + TSTransform string +} + +// FieldTags allow to add any tags to a field. +type FieldTags map[string][]*structtag.Tag + +// Set tags to struct fields +func AddFieldTags(t reflect.Type, fieldTags *FieldTags) reflect.Type { + sf := make([]reflect.StructField, 0) + for i := 0; i < t.NumField(); i++ { + sf = append(sf, t.Field(i)) + + if newTags, ok := (*fieldTags)[t.Field(i).Name]; ok { + // parse field Tag + tagString := string(t.Field(i).Tag) + tags, err := structtag.Parse(tagString) + if err != nil { + fmt.Printf("Error parsing %q: %v\n", tagString, err) + continue + } + // set newTags + for _, tag := range newTags { + err := tags.Set(tag) + if err != nil { + fmt.Printf("Error setting tag %q: %v\n", tag, err) + } + } + sf[i].Tag = reflect.StructTag(tags.String()) + } + } + return reflect.StructOf(sf) +} + +// Create anonymous struct with provided new tags added to all fields +func TagAll(t reflect.Type, newTags []string) reflect.Type { + sf := make([]reflect.StructField, 0) + for i := 0; i < t.NumField(); i++ { + sf = append(sf, t.Field(i)) + + // parse field Tag + tagString := string(t.Field(i).Tag) + tags, err := structtag.Parse(tagString) + if err != nil { + fmt.Printf("Error parsing %q: %v\n", tagString, err) + continue + } + // add newTags to json tag + jsonTag, err := tags.Get("json") + if err != nil { + fmt.Printf("Error getting json tag: %s\n", err) + continue + } + jsonTag.Options = newTags + err = tags.Set(jsonTag) + if err != nil { + fmt.Printf("Error setting %q tags: %s\n", newTags, err) + } + sf[i].Tag = reflect.StructTag(tags.String()) + } + return reflect.StructOf(sf) +} + +// StructType stores settings for transforming one Golang struct. +type StructType struct { + Type reflect.Type + FieldOptions map[reflect.Type]TypeOptions + Name string +} + +func NewStruct(i interface{}) *StructType { + return &StructType{ + Type: reflect.TypeOf(i), + } +} + +func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType { + if st.FieldOptions == nil { + st.FieldOptions = map[reflect.Type]TypeOptions{} + } + var typ reflect.Type + if ty, is := i.(reflect.Type); is { + typ = ty + } else { + typ = reflect.TypeOf(i) + } + st.FieldOptions[typ] = opts + return st +} + +type EnumType struct { + Type reflect.Type +} + +type enumElement struct { + value interface{} + name string +} + +type TypeScriptify struct { + Prefix string + Suffix string + Indent string + CreateFromMethod bool + CreateConstructor bool + BackupDir string // If empty no backup + DontExport bool + CreateInterface bool + ReadOnlyFields bool + customImports []string + + structTypes []StructType + enumTypes []EnumType + enums map[reflect.Type][]enumElement + kinds map[reflect.Kind]string + + fieldTypeOptions map[reflect.Type]TypeOptions + + // throwaway, used when converting + alreadyConverted map[reflect.Type]bool +} + +func New() *TypeScriptify { + result := new(TypeScriptify) + result.Indent = "\t" + result.BackupDir = "." + + kinds := make(map[reflect.Kind]string) + + kinds[reflect.Bool] = "boolean" + kinds[reflect.Interface] = "any" + + kinds[reflect.Int] = "number" + kinds[reflect.Int8] = "number" + kinds[reflect.Int16] = "number" + kinds[reflect.Int32] = "number" + kinds[reflect.Int64] = "number" + kinds[reflect.Uint] = "number" + kinds[reflect.Uint8] = "number" + kinds[reflect.Uint16] = "number" + kinds[reflect.Uint32] = "number" + kinds[reflect.Uint64] = "number" + kinds[reflect.Float32] = "number" + kinds[reflect.Float64] = "number" + + kinds[reflect.String] = "string" + + result.kinds = kinds + + result.Indent = " " + result.CreateFromMethod = false + result.CreateConstructor = true + + return result +} + +func deepFields(typeOf reflect.Type) []reflect.StructField { + fields := make([]reflect.StructField, 0) + + if typeOf.Kind() == reflect.Ptr { + typeOf = typeOf.Elem() + } + + if typeOf.Kind() != reflect.Struct { + return fields + } + + for i := 0; i < typeOf.NumField(); i++ { + f := typeOf.Field(i) + + kind := f.Type.Kind() + if f.Anonymous && kind == reflect.Struct { + //fmt.Println(v.Interface()) + fields = append(fields, deepFields(f.Type)...) + } else if f.Anonymous && kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct { + //fmt.Println(v.Interface()) + fields = append(fields, deepFields(f.Type.Elem())...) + } else { + fields = append(fields, f) + } + } + + return fields +} + +func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) { + fmt.Printf(strings.Repeat(" ", depth)+s+"\n", args...) +} + +// ManageType can define custom options for fields of a specified type. +// +// This can be used instead of setting ts_type and ts_transform for all fields of a certain type. +func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify { + var typ reflect.Type + switch t := fld.(type) { + case reflect.Type: + typ = t + default: + typ = reflect.TypeOf(fld) + } + if t.fieldTypeOptions == nil { + t.fieldTypeOptions = map[reflect.Type]TypeOptions{} + } + t.fieldTypeOptions[typ] = opts + return t +} + +func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify { + t.CreateFromMethod = b + return t +} + +func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify { + t.CreateInterface = b + return t +} + +func (t *TypeScriptify) WithReadonlyFields(b bool) *TypeScriptify { + t.ReadOnlyFields = b + return t +} + +func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify { + t.CreateConstructor = b + return t +} + +func (t *TypeScriptify) WithIndent(i string) *TypeScriptify { + t.Indent = i + return t +} + +func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify { + t.BackupDir = b + return t +} + +func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify { + t.Prefix = p + return t +} + +func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify { + t.Suffix = s + return t +} + +func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify { + switch ty := obj.(type) { + case StructType: + t.structTypes = append(t.structTypes, ty) + case *StructType: + t.structTypes = append(t.structTypes, *ty) + case reflect.Type: + t.AddType(ty) + default: + t.AddType(reflect.TypeOf(obj)) + } + return t +} + +func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify { + t.structTypes = append(t.structTypes, StructType{Type: typeOf}) + return t +} + +func (t *TypeScriptify) AddTypeWithName(typeOf reflect.Type, name string) *TypeScriptify { + t.structTypes = append(t.structTypes, StructType{Type: typeOf, Name: name}) + return t +} + +func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) { + keyType := field.Type.Key() + valueType := field.Type.Elem() + valueTypeName := valueType.Name() + if name, ok := t.types[valueType.Kind()]; ok { + valueTypeName = name + } + if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice { + valueTypeName = valueType.Elem().Name() + "[]" + } + if valueType.Kind() == reflect.Ptr { + valueTypeName = valueType.Elem().Name() + } + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + + keyTypeStr := keyType.Name() + // Key should always be string, no need for this: + // _, isSimple := t.types[keyType.Kind()] + // if !isSimple { + // keyTypeStr = t.prefix + keyType.Name() + t.suffix + // } + + if valueType.Kind() == reflect.Struct { + t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, t.prefix+valueTypeName)) + 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)) + } else { + t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, valueTypeName)) + t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = source[\"%s\"];", t.indent, t.indent, strippedFieldName, strippedFieldName)) + } +} + +func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify { + if t.enums == nil { + t.enums = map[reflect.Type][]enumElement{} + } + items := reflect.ValueOf(values) + if items.Kind() != reflect.Slice { + panic(fmt.Sprintf("Values for %T isn't a slice", values)) + } + + var elements []enumElement + for i := 0; i < items.Len(); i++ { + item := items.Index(i) + + var el enumElement + if item.Kind() == reflect.Struct { + r := reflector.New(item.Interface()) + val, err := r.Field("Value").Get() + if err != nil { + panic(fmt.Sprint("missing Type field in ", item.Type().String())) + } + name, err := r.Field("TSName").Get() + if err != nil { + panic(fmt.Sprint("missing TSName field in ", item.Type().String())) + } + el.value = val + el.name = name.(string) + } else { + el.value = item.Interface() + if tsNamer, is := item.Interface().(TSNamer); is { + el.name = tsNamer.TSName() + } else { + panic(fmt.Sprint(item.Type().String(), " has no TSName method")) + } + } + + elements = append(elements, el) + } + ty := reflect.TypeOf(elements[0].value) + t.enums[ty] = elements + t.enumTypes = append(t.enumTypes, EnumType{Type: ty}) + + return t +} + +// AddEnumValues is deprecated, use `AddEnum()` +func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify { + t.AddEnum(values) + return t +} + +func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) { + if t.CreateFromMethod { + fmt.Fprintln(os.Stderr, "FromMethod METHOD IS DEPRECATED AND WILL BE REMOVED!!!!!!") + } + + t.alreadyConverted = make(map[reflect.Type]bool) + depth := 0 + + result := "" + if len(t.customImports) > 0 { + // Put the custom imports, i.e.: `import Decimal from 'decimal.js'` + for _, cimport := range t.customImports { + result += cimport + "\n" + } + } + + for _, enumTyp := range t.enumTypes { + elements := t.enums[enumTyp.Type] + typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements) + if err != nil { + return "", err + } + result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") + } + + for _, strctTyp := range t.structTypes { + typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode) + if err != nil { + return "", err + } + result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") + } + return result, nil +} + +func loadCustomCode(fileName string) (map[string]string, error) { + result := make(map[string]string) + f, err := os.Open(fileName) + if err != nil { + if os.IsNotExist(err) { + return result, nil + } + return result, err + } + defer f.Close() + + bytes, err := io.ReadAll(f) + if err != nil { + return result, err + } + + var currentName string + var currentValue string + lines := strings.Split(string(bytes), "\n") + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") { + currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1) + currentValue = "" + } else if trimmedLine == "//[end]" { + result[currentName] = strings.TrimRight(currentValue, " \t\r\n") + currentName = "" + currentValue = "" + } else if len(currentName) > 0 { + currentValue += line + "\n" + } + } + + return result, nil +} + +func (t TypeScriptify) backup(fileName string) error { + fileIn, err := os.Open(fileName) + if err != nil { + if !os.IsNotExist(err) { + return err + } + // No neet to backup, just return: + return nil + } + defer fileIn.Close() + + bytes, err := io.ReadAll(fileIn) + if err != nil { + return err + } + + _, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99"))) + if t.BackupDir != "" { + backupFn = path.Join(t.BackupDir, backupFn) + } + + return os.WriteFile(backupFn, bytes, os.FileMode(0700)) +} + +func (t TypeScriptify) ConvertToFile(fileName string) error { + if len(t.BackupDir) > 0 { + err := t.backup(fileName) + if err != nil { + return err + } + } + + customCode, err := loadCustomCode(fileName) + if err != nil { + return err + } + + f, err := os.Create(fileName) + if err != nil { + return err + } + defer f.Close() + + converted, err := t.Convert(customCode) + if err != nil { + return err + } + + if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil { + return err + } + if _, err := f.WriteString(converted); err != nil { + return err + } + if err != nil { + return err + } + + return nil +} + +type TSNamer interface { + TSName() string +} + +func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) { + t.logf(depth, "Converting enum %s", typeOf.String()) + if _, found := t.alreadyConverted[typeOf]; found { // Already converted + return "", nil + } + t.alreadyConverted[typeOf] = true + + entityName := t.Prefix + typeOf.Name() + t.Suffix + result := "enum " + entityName + " {\n" + + for _, val := range elements { + result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value) + } + + result += "}" + + if !t.DontExport { + result = "export " + result + } + + return result, nil +} + +func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions { + // By default use options defined by tags: + opts := TypeOptions{ + TSTransform: field.Tag.Get(tsTransformTag), + TSType: field.Tag.Get(tsType), + TSDoc: field.Tag.Get(tsDocTag), + } + + overrides := []TypeOptions{} + + // But there is maybe an struct-specific override: + for _, strct := range t.structTypes { + if strct.FieldOptions == nil { + continue + } + if strct.Type == structType { + if fldOpts, found := strct.FieldOptions[field.Type]; found { + overrides = append(overrides, fldOpts) + } + } + } + + if fldOpts, found := t.fieldTypeOptions[field.Type]; found { + overrides = append(overrides, fldOpts) + } + + for _, o := range overrides { + if o.TSTransform != "" { + opts.TSTransform = o.TSTransform + } + if o.TSType != "" { + opts.TSType = o.TSType + } + } + + return opts +} + +func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string { + jsonFieldName := "" + jsonTag := field.Tag.Get("json") + if len(jsonTag) > 0 { + jsonTagParts := strings.Split(jsonTag, ",") + if len(jsonTagParts) > 0 { + jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent) + } + hasOmitEmpty := false + ignored := false + for _, t := range jsonTagParts { + if t == "" { + break + } + if t == "omitempty" { + hasOmitEmpty = true + break + } + if t == "-" { + ignored = true + break + } + } + if !ignored && isPtr || hasOmitEmpty { + jsonFieldName = fmt.Sprintf("%s?", jsonFieldName) + } + } else if /*field.IsExported()*/ field.PkgPath == "" { + jsonFieldName = field.Name + } + return jsonFieldName +} + +func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) { + if _, found := t.alreadyConverted[typeOf]; found { // Already converted + return "", nil + } + t.logf(depth, "Converting type %s", typeOf.String()) + + t.alreadyConverted[typeOf] = true + + typeName := typeOf.Name() + if typeName == "" { + idx := slices.IndexFunc(t.structTypes, + func(structType StructType) bool { + return typeOf == structType.Type + }) + if idx >= 0 && t.structTypes[idx].Name != "" { + typeName = t.structTypes[idx].Name + } else { + fmt.Println("Use .AddTypeWithName to avoid UnknownStruct") + typeName = "UnknownStruct" + } + } + entityName := t.Prefix + typeName + t.Suffix + result := "" + if t.CreateInterface { + result += fmt.Sprintf("interface %s {\n", entityName) + } else { + result += fmt.Sprintf("class %s {\n", entityName) + } + if !t.DontExport { + result = "export " + result + } + builder := typeScriptClassBuilder{ + types: t.kinds, + indent: t.Indent, + prefix: t.Prefix, + suffix: t.Suffix, + readOnlyFields: t.ReadOnlyFields, + } + + fields := deepFields(typeOf) + for _, field := range fields { + isPtr := field.Type.Kind() == reflect.Ptr + if isPtr { + field.Type = field.Type.Elem() + } + jsonFieldName := t.getJSONFieldName(field, isPtr) + if len(jsonFieldName) == 0 || jsonFieldName == "-" { + continue + } + + var err error + fldOpts := t.getFieldOptions(typeOf, field) + if fldOpts.TSDoc != "" { + builder.addFieldDefinitionLine("/** " + fldOpts.TSDoc + " */") + } + if fldOpts.TSTransform != "" { + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } else if _, isEnum := t.enums[field.Type]; isEnum { + t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name) + builder.AddEnumField(jsonFieldName, field) + } else if fldOpts.TSType != "" { // Struct: + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } else if field.Type.Kind() == reflect.Struct { // Struct: + t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) + typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + builder.AddStructField(jsonFieldName, field) + } else if field.Type.Kind() == reflect.Map { + t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name) + // Also convert map key types if needed + var keyTypeToConvert reflect.Type + switch field.Type.Key().Kind() { + case reflect.Struct: + keyTypeToConvert = field.Type.Key() + case reflect.Ptr: + keyTypeToConvert = field.Type.Key().Elem() + } + if keyTypeToConvert != nil { + typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + } + // Also convert map value types if needed + var valueTypeToConvert reflect.Type + switch field.Type.Elem().Kind() { + case reflect.Struct: + valueTypeToConvert = field.Type.Elem() + case reflect.Ptr: + valueTypeToConvert = field.Type.Elem().Elem() + } + if valueTypeToConvert != nil { + typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + } + + builder.AddMapField(jsonFieldName, field) + } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice: + if field.Type.Elem().Kind() == reflect.Ptr { //extract ptr type + field.Type = field.Type.Elem() + } + + arrayDepth := 1 + for field.Type.Elem().Kind() == reflect.Slice { // Slice of slices: + field.Type = field.Type.Elem() + arrayDepth++ + } + + if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs: + t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) + typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth) + } else { // Slice of simple fields: + t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts) + } + } else { // Simple field: + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } + if err != nil { + return "", err + } + } + + if t.CreateFromMethod { + t.CreateConstructor = true + } + + result += strings.Join(builder.fields, "\n") + "\n" + if !t.CreateInterface { + constructorBody := strings.Join(builder.constructorBody, "\n") + needsConvertValue := strings.Contains(constructorBody, "this.convertValues") + if t.CreateFromMethod { + result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent) + result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName) + result += fmt.Sprintf("%s}\n", t.Indent) + } + if t.CreateConstructor { + result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent) + result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n" + result += constructorBody + "\n" + result += fmt.Sprintf("%s}\n", t.Indent) + } + if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) { + result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n" + } + } + + if customCode != nil { + code := customCode[entityName] + if len(code) != 0 { + result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n" + } + } + + result += "}" + + return result, nil +} + +func (t *TypeScriptify) AddImport(i string) { + for _, cimport := range t.customImports { + if cimport == i { + return + } + } + + t.customImports = append(t.customImports, i) +} + +type typeScriptClassBuilder struct { + types map[reflect.Kind]string + indent string + fields []string + createFromMethodBody []string + constructorBody []string + prefix, suffix string + readOnlyFields bool +} + +func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error { + fieldType, kind := field.Type.Elem().Name(), field.Type.Elem().Kind() + typeScriptType := t.types[kind] + + if len(fieldName) > 0 { + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + if len(opts.TSType) > 0 { + t.addField(fieldName, opts.TSType) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + return nil + } else if len(typeScriptType) > 0 { + t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth))) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + return nil + } + } + + return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) +} + +func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error { + fieldType, kind := field.Type.Name(), field.Type.Kind() + + typeScriptType := t.types[kind] + if len(opts.TSType) > 0 { + typeScriptType = opts.TSType + } + + if len(typeScriptType) > 0 && len(fieldName) > 0 { + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, typeScriptType) + if opts.TSTransform == "" { + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + } else { + val := fmt.Sprintf(`source["%s"]`, strippedFieldName) + expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1) + t.addInitializerFieldLine(strippedFieldName, expression) + } + return nil + } + + return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) +} + +func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) { + fieldType := field.Type.Name() + t.addField(fieldName, t.prefix+fieldType+t.suffix) + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) +} + +func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField) { + fieldType := field.Type.Name() + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, t.prefix+fieldType+t.suffix) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) +} + +func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) { + fieldType := field.Type.Elem().Name() + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth))) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) +} + +func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) { + t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result.", fld, " = ", initializer, ";")) + t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this.", fld, " = ", initializer, ";")) +} + +func (t *typeScriptClassBuilder) addFieldDefinitionLine(line string) { + t.fields = append(t.fields, t.indent+line) +} + +func (t *typeScriptClassBuilder) addField(fld, fldType string) { + ro := "" + if t.readOnlyFields { + ro = "readonly " + } + t.fields = append(t.fields, fmt.Sprint(t.indent, ro, fld, ": ", fldType, ";")) +} diff --git a/typescriptify/typescriptify_test.go b/typescriptify/typescriptify_test.go index 75adde9..bdd93b7 100644 --- a/typescriptify/typescriptify_test.go +++ b/typescriptify/typescriptify_test.go @@ -1,1020 +1,1066 @@ -package typescriptify - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "reflect" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -type Address struct { - // Used in html - Duration float64 `json:"duration"` - Text1 string `json:"text,omitempty"` - // Ignored: - Text2 string `json:",omitempty"` - Text3 string `json:"-"` -} - -type Dummy struct { - Something string `json:"something"` -} - -type HasName struct { - Name string `json:"name"` -} - -type Person struct { - HasName - Nicknames []string `json:"nicknames"` - Addresses []Address `json:"addresses"` - Address *Address `json:"address"` - Metadata string `json:"metadata" ts_type:"{[key:string]:string}" ts_transform:"JSON.parse(__VALUE__ || \"{}\")"` - Friends []*Person `json:"friends"` - Dummy Dummy `json:"a"` -} - -func TestTypescriptifyWithTypes(t *testing.T) { - t.Parallel() - converter := New() - - converter.AddType(reflect.TypeOf(Person{})) - converter.CreateConstructor = false - converter.BackupDir = "" - - desiredResult := `export class Dummy { - something: string; -} -export class Address { - duration: number; - text?: string; -} -export class Person { - name: string; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; - a: Dummy; -}` - testConverter(t, converter, false, desiredResult, nil) -} - -func TestTypescriptifyWithCustomImports(t *testing.T) { - t.Parallel() - converter := New() - - converter.AddType(reflect.TypeOf(Person{})) - converter.BackupDir = "" - converter.AddImport("//import { Decimal } from 'decimal.js'") - converter.CreateConstructor = false - - desiredResult := ` -//import { Decimal } from 'decimal.js' - -export class Dummy { - something: string; -} -export class Address { - duration: number; - text?: string; -} -export class Person { - name: string; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; - a: Dummy; -}` - testConverter(t, converter, false, desiredResult, nil) -} - -func TestTypescriptifyWithInstances(t *testing.T) { - t.Parallel() - converter := New() - - converter.Add(Person{}) - converter.Add(Dummy{}) - converter.DontExport = true - converter.BackupDir = "" - converter.CreateConstructor = false - - desiredResult := `class Dummy { - something: string; -} -class Address { - duration: number; - text?: string; -} -class Person { - name: string; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; - a: Dummy; -}` - testConverter(t, converter, false, desiredResult, nil) -} - -func TestTypescriptifyWithInterfaces(t *testing.T) { - t.Parallel() - converter := New() - - converter.Add(Person{}) - converter.Add(Dummy{}) - converter.DontExport = true - converter.BackupDir = "" - converter.CreateInterface = true - - desiredResult := `interface Dummy { - something: string; -} -interface Address { - duration: number; - text?: string; -} -interface Person { - name: string; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; - a: Dummy; -}` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestTypescriptifyWithDoubleClasses(t *testing.T) { - t.Parallel() - converter := New() - - converter.AddType(reflect.TypeOf(Person{})) - converter.AddType(reflect.TypeOf(Person{})) - converter.CreateConstructor = false - converter.BackupDir = "" - - desiredResult := `export class Dummy { - something: string; -} -export class Address { - duration: number; - text?: string; -} -export class Person { - name: string; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; - a: Dummy; -}` - testConverter(t, converter, false, desiredResult, nil) -} - -func TestWithPrefixes(t *testing.T) { - t.Parallel() - converter := New() - - converter.Prefix = "test_" - converter.Suffix = "_test" - - converter.Add(Person{}) - converter.DontExport = true - converter.BackupDir = "" - - desiredResult := `class test_Dummy_test { - something: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.something = source["something"]; - } -} -class test_Address_test { - duration: number; - text?: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.duration = source["duration"]; - this.text = source["text"]; - } -} -class test_Person_test { - name: string; - nicknames: string[]; - addresses: test_Address_test[]; - address?: test_Address_test; - metadata: {[key:string]:string}; - friends: test_Person_test[]; - a: test_Dummy_test; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.name = source["name"]; - this.nicknames = source["nicknames"]; - this.addresses = this.convertValues(source["addresses"], test_Address_test); - this.address = this.convertValues(source["address"], test_Address_test); - this.metadata = JSON.parse(source["metadata"] || "{}"); - this.friends = this.convertValues(source["friends"], test_Person_test); - this.a = this.convertValues(source["a"], test_Dummy_test); - } - - ` + tsConvertValuesFunc + ` -}` - jsn := jsonizeOrPanic(Person{ - Address: &Address{Text1: "txt1"}, - Addresses: []Address{{Text1: "111"}}, - Metadata: `{"something": "aaa"}`, - }) - testConverter(t, converter, true, desiredResult, []string{ - `new test_Person_test()`, - `JSON.stringify(new test_Person_test()?.metadata) === "{}"`, - `!(new test_Person_test()?.address)`, - `!(new test_Person_test()?.addresses)`, - `!(new test_Person_test()?.addresses)`, - - `new test_Person_test(` + jsn + ` as any)`, - `new test_Person_test(` + jsn + ` as any)?.metadata?.something === "aaa"`, - `(new test_Person_test(` + jsn + ` as any)?.address as test_Address_test).text === "txt1"`, - `new test_Person_test(` + jsn + ` as any)?.addresses?.length === 1`, - `(new test_Person_test(` + jsn + ` as any)?.addresses[0] as test_Address_test)?.text === "111"`, - }) -} - -func testConverter(t *testing.T, converter *TypeScriptify, strictMode bool, desiredResult string, tsExpressionAndDesiredResults []string) { - typeScriptCode, err := converter.Convert(nil) - if err != nil { - panic(err.Error()) - } - - fmt.Println("----------------------------------------------------------------------------------------------------") - fmt.Println(desiredResult) - fmt.Println("----------------------------------------------------------------------------------------------------") - fmt.Println(typeScriptCode) - fmt.Println("----------------------------------------------------------------------------------------------------") - - desiredResult = strings.TrimSpace(desiredResult) - typeScriptCode = strings.Trim(typeScriptCode, " \t\n\r") - if typeScriptCode != desiredResult { - gotLines1 := strings.Split(typeScriptCode, "\n") - expectedLines2 := strings.Split(desiredResult, "\n") - - max := len(gotLines1) - if len(expectedLines2) > max { - max = len(expectedLines2) - } - - for i := 0; i < max; i++ { - var gotLine, expectedLine string - if i < len(gotLines1) { - gotLine = gotLines1[i] - } - if i < len(expectedLines2) { - expectedLine = expectedLines2[i] - } - if assert.Equal(t, strings.TrimSpace(expectedLine), strings.TrimSpace(gotLine), "line #%d", 1+i) { - fmt.Printf("OK: %s\n", gotLine) - } else { - t.FailNow() - } - } - } - - if t.Failed() { - t.FailNow() - } - - testTypescriptExpression(t, strictMode, typeScriptCode, tsExpressionAndDesiredResults) -} - -func testTypescriptExpression(t *testing.T, strictMode bool, baseScript string, tsExpressionAndDesiredResults []string) { - f, err := os.CreateTemp(os.TempDir(), "*.ts") - assert.Nil(t, err) - assert.NotNil(t, f) - - if t.Failed() { - t.FailNow() - } - - _, _ = f.WriteString(baseScript) - _, _ = f.WriteString("\n") - for n, expr := range tsExpressionAndDesiredResults { - _, _ = f.WriteString("// " + expr + "\n") - _, _ = f.WriteString(`if (` + expr + `) { console.log("#` + fmt.Sprint(1+n) + ` OK") } else { throw new Error() }`) - _, _ = f.WriteString("\n\n") - } - - fmt.Println("tmp ts: ", f.Name()) - var byts []byte - if strictMode { - byts, err = exec.Command("tsc", "--strict", f.Name()).CombinedOutput() - } else { - byts, err = exec.Command("tsc", f.Name()).CombinedOutput() - } - assert.Nil(t, err, string(byts)) - - jsFile := strings.Replace(f.Name(), ".ts", ".js", 1) - fmt.Println("executing:", jsFile) - byts, err = exec.Command("node", jsFile).CombinedOutput() - assert.Nil(t, err, string(byts)) -} - -func TestTypescriptifyCustomType(t *testing.T) { - t.Parallel() - type TestCustomType struct { - Map map[string]int `json:"map" ts_type:"{[key: string]: number}"` - } - - converter := New() - - converter.AddType(reflect.TypeOf(TestCustomType{})) - converter.BackupDir = "" - converter.CreateConstructor = false - - desiredResult := `export class TestCustomType { - map: {[key: string]: number}; -}` - testConverter(t, converter, false, desiredResult, nil) -} - -func TestDate(t *testing.T) { - t.Parallel() - type TestCustomType struct { - Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"` - } - - converter := New() - converter.AddType(reflect.TypeOf(TestCustomType{})) - converter.BackupDir = "" - - desiredResult := `export class TestCustomType { - time: Date; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.time = new Date(source["time"]); - } -}` - - jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)}) - testConverter(t, converter, true, desiredResult, []string{ - `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`, - //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`, - `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`, - }) -} - -func TestDateWithoutTags(t *testing.T) { - t.Parallel() - type TestCustomType struct { - Time time.Time `json:"time"` - } - - // Test with custom field options defined per-one-struct: - converter1 := New() - converter1.Add(NewStruct(TestCustomType{}).WithFieldOpts(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"})) - converter1.BackupDir = "" - - // Test with custom field options defined globally: - converter2 := New() - converter2.Add(reflect.TypeOf(TestCustomType{})) - converter2.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"}) - converter2.BackupDir = "" - - for _, converter := range []*TypeScriptify{converter1, converter2} { - desiredResult := `export class TestCustomType { - time: Date; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.time = new Date(source["time"]); - } -}` - - jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)}) - testConverter(t, converter, true, desiredResult, []string{ - `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`, - //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`, - `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`, - }) - } -} - -func TestRecursive(t *testing.T) { - t.Parallel() - type Test struct { - Children []Test `json:"children"` - } - - converter := New() - - converter.AddType(reflect.TypeOf(Test{})) - converter.BackupDir = "" - - desiredResult := `export class Test { - children: Test[]; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.children = this.convertValues(source["children"], Test); - } - - ` + tsConvertValuesFunc + ` -}` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestArrayOfArrays(t *testing.T) { - t.Parallel() - type Key struct { - Key string `json:"key"` - } - type Keyboard struct { - Keys [][]Key `json:"keys"` - } - - converter := New() - - converter.AddType(reflect.TypeOf(Keyboard{})) - converter.BackupDir = "" - - desiredResult := `export class Key { - key: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.key = source["key"]; - } -} -export class Keyboard { - keys: Key[][]; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.keys = this.convertValues(source["keys"], Key); - } - - ` + tsConvertValuesFunc + ` -}` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestFixedArray(t *testing.T) { - t.Parallel() - type Sub struct{} - type Tmp struct { - Arr [3]string `json:"arr"` - Arr2 [3]Sub `json:"arr2"` - } - - converter := New() - - converter.AddType(reflect.TypeOf(Tmp{})) - converter.BackupDir = "" - - desiredResult := `export class Sub { - - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - - } -} -export class Tmp { - arr: string[]; - arr2: Sub[]; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.arr = source["arr"]; - this.arr2 = this.convertValues(source["arr2"], Sub); - } - - ` + tsConvertValuesFunc + ` -} -` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestAny(t *testing.T) { - t.Parallel() - type Test struct { - Any interface{} `json:"field"` - } - - converter := New() - - converter.AddType(reflect.TypeOf(Test{})) - converter.BackupDir = "" - - desiredResult := `export class Test { - field: any; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.field = source["field"]; - } -}` - testConverter(t, converter, true, desiredResult, nil) -} - -type NumberTime time.Time - -func (t NumberTime) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("%d", time.Time(t).Unix())), nil -} - -func TestTypeAlias(t *testing.T) { - t.Parallel() - type Person struct { - Birth NumberTime `json:"birth" ts_type:"number"` - } - - converter := New() - - converter.AddType(reflect.TypeOf(Person{})) - converter.BackupDir = "" - converter.CreateConstructor = false - - desiredResult := `export class Person { - birth: number; -}` - testConverter(t, converter, false, desiredResult, nil) -} - -type MSTime struct { - time.Time -} - -func (MSTime) UnmarshalJSON([]byte) error { return nil } -func (MSTime) MarshalJSON() ([]byte, error) { return []byte("1111"), nil } - -func TestOverrideCustomType(t *testing.T) { - t.Parallel() - - type SomeStruct struct { - Time MSTime `json:"time" ts_type:"number"` - } - var _ json.Marshaler = new(MSTime) - var _ json.Unmarshaler = new(MSTime) - - converter := New() - - converter.AddType(reflect.TypeOf(SomeStruct{})) - converter.BackupDir = "" - converter.CreateConstructor = false - - desiredResult := `export class SomeStruct { - time: number; -}` - testConverter(t, converter, false, desiredResult, nil) - - byts, _ := json.Marshal(SomeStruct{Time: MSTime{Time: time.Now()}}) - assert.Equal(t, `{"time":1111}`, string(byts)) -} - -type Weekday int - -const ( - Sunday Weekday = iota - Monday - Tuesday - Wednesday - Thursday - Friday - Saturday -) - -func (w Weekday) TSName() string { - switch w { - case Sunday: - return "SUNDAY" - case Monday: - return "MONDAY" - case Tuesday: - return "TUESDAY" - case Wednesday: - return "WEDNESDAY" - case Thursday: - return "THURSDAY" - case Friday: - return "FRIDAY" - case Saturday: - return "SATURDAY" - default: - return "???" - } -} - -// One way to specify enums is to list all values and then every one must have a TSName() method -var allWeekdaysV1 = []Weekday{ - Sunday, - Monday, - Tuesday, - Wednesday, - Thursday, - Friday, - Saturday, -} - -// Another way to specify enums: -var allWeekdaysV2 = []struct { - Value Weekday - TSName string -}{ - {Sunday, "SUNDAY"}, - {Monday, "MONDAY"}, - {Tuesday, "TUESDAY"}, - {Wednesday, "WEDNESDAY"}, - {Thursday, "THURSDAY"}, - {Friday, "FRIDAY"}, - {Saturday, "SATURDAY"}, -} - -type Holliday struct { - Name string `json:"name"` - Weekday Weekday `json:"weekday"` -} - -func TestEnum(t *testing.T) { - t.Parallel() - for _, allWeekdays := range []interface{}{allWeekdaysV1, allWeekdaysV2} { - converter := New(). - AddType(reflect.TypeOf(Holliday{})). - AddEnum(allWeekdays). - WithConstructor(true). - WithBackupDir("") - - desiredResult := `export enum Weekday { - SUNDAY = 0, - MONDAY = 1, - TUESDAY = 2, - WEDNESDAY = 3, - THURSDAY = 4, - FRIDAY = 5, - SATURDAY = 6, -} -export class Holliday { - name: string; - weekday: Weekday; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.name = source["name"]; - this.weekday = source["weekday"]; - } -}` - testConverter(t, converter, true, desiredResult, nil) - } -} - -type Gender string - -const ( - MaleStr Gender = "m" - FemaleStr Gender = "f" -) - -var allGenders = []struct { - Value Gender - TSName string -}{ - {MaleStr, "MALE"}, - {FemaleStr, "FEMALE"}, -} - -func TestEnumWithStringValues(t *testing.T) { - t.Parallel() - converter := New(). - AddEnum(allGenders). - WithConstructor(false). - WithBackupDir("") - - desiredResult := ` -export enum Gender { - MALE = "m", - FEMALE = "f", -} -` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestConstructorWithReferences(t *testing.T) { - t.Parallel() - converter := New(). - AddType(reflect.TypeOf(Person{})). - AddEnum(allWeekdaysV2). - WithConstructor(true). - WithBackupDir("") - - desiredResult := `export enum Weekday { - SUNDAY = 0, - MONDAY = 1, - TUESDAY = 2, - WEDNESDAY = 3, - THURSDAY = 4, - FRIDAY = 5, - SATURDAY = 6, -} -export class Dummy { - something: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.something = source["something"]; - } -} -export class Address { - duration: number; - text?: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.duration = source["duration"]; - this.text = source["text"]; - } -} -export class Person { - name: string; - nicknames: string[]; - addresses: Address[]; - address?: Address; - metadata: {[key:string]:string}; - friends: Person[]; - a: Dummy; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.name = source["name"]; - this.nicknames = source["nicknames"]; - this.addresses = this.convertValues(source["addresses"], Address); - this.address = this.convertValues(source["address"], Address); - this.metadata = JSON.parse(source["metadata"] || "{}"); - this.friends = this.convertValues(source["friends"], Person); - this.a = this.convertValues(source["a"], Dummy); - } - - ` + tsConvertValuesFunc + ` -}` - testConverter(t, converter, true, desiredResult, nil) -} - -type WithMap struct { - Map map[string]int `json:"simpleMap"` - MapObjects map[string]Address `json:"mapObjects"` - PtrMap *map[string]Address `json:"ptrMapObjects"` -} - -func TestMaps(t *testing.T) { - t.Parallel() - converter := New(). - AddType(reflect.TypeOf(WithMap{})). - WithConstructor(true). - WithPrefix("API_"). - WithBackupDir("") - - desiredResult := ` - export class API_Address { - duration: number; - text?: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.duration = source["duration"]; - this.text = source["text"]; - } - } - export class API_WithMap { - simpleMap: {[key: string]: number}; - mapObjects: {[key: string]: API_Address}; - ptrMapObjects?: {[key: string]: API_Address}; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.simpleMap = source["simpleMap"]; - this.mapObjects = this.convertValues(source["mapObjects"], API_Address, true); - this.ptrMapObjects = this.convertValues(source["ptrMapObjects"], API_Address, true); - } - - ` + tsConvertValuesFunc + ` - } -` - - json := WithMap{ - Map: map[string]int{"aaa": 1}, - MapObjects: map[string]Address{"bbb": {Duration: 1.0, Text1: "txt1"}}, - PtrMap: &map[string]Address{"ccc": {Duration: 2.0, Text1: "txt2"}}, - } - - testConverter(t, converter, true, desiredResult, []string{ - `new API_WithMap(` + jsonizeOrPanic(json) + `).simpleMap.aaa == 1`, - `(new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_Address`, - `!((new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_WithMap)`, - `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.duration == 1`, - `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.text === "txt1"`, - `(new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_Address`, - `!((new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_WithMap)`, - `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.duration === 2`, - `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.text === "txt2"`, - }) -} - -func TestPTR(t *testing.T) { - t.Parallel() - type Person struct { - Name *string `json:"name"` - } - - converter := New() - converter.BackupDir = "" - converter.CreateConstructor = false - converter.Add(Person{}) - - desiredResult := `export class Person { - name?: string; -}` - testConverter(t, converter, true, desiredResult, nil) -} - -type PersonWithPtrName struct { - *HasName -} - -func TestAnonymousPtr(t *testing.T) { - t.Parallel() - var p PersonWithPtrName - p.HasName = &HasName{} - p.Name = "JKLJKL" - converter := New(). - AddType(reflect.TypeOf(PersonWithPtrName{})). - WithConstructor(true). - WithBackupDir("") - - desiredResult := ` - export class PersonWithPtrName { - name: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.name = source["name"]; - } - } -` - testConverter(t, converter, true, desiredResult, nil) -} - -func jsonizeOrPanic(i interface{}) string { - byts, err := json.Marshal(i) - if err != nil { - panic(err) - } - return string(byts) -} - -func TestTestConverter(t *testing.T) { - t.Parallel() - - ts := `class Converter { - ` + tsConvertValuesFunc + ` -} -const converter = new Converter(); - -class Address { - street: string; - number: number; - - constructor(a: any) { - this.street = a["street"]; - this.number = a["number"]; - } -} -` - - testTypescriptExpression(t, true, ts, []string{ - `(converter.convertValues(null, Address)) === null`, - `(converter.convertValues([], Address)).length === 0`, - `(converter.convertValues({}, Address)) instanceof Address`, - `!(converter.convertValues({}, Address, true) instanceof Address)`, - - `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[]).length == 1`, - `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0] instanceof Address`, - `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].number === 19`, - `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].street === "aaa"`, - - `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[]).length == 1`, - `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0] instanceof Address`, - `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].number === 19`, - `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].street === "aaa"`, - - `Object.keys((converter.convertValues({"first": {street: "aaa", number: 19}}, Address, true) as {[_: string]: Address})).length == 1`, - `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"] instanceof Address`, - `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].number === 19`, - `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].street === "aaa"`, - }) -} - -func TestIgnoredPTR(t *testing.T) { - t.Parallel() - type PersonWithIgnoredPtr struct { - Name string `json:"name"` - Nickname *string `json:"-"` - } - - converter := New() - converter.BackupDir = "" - converter.Add(PersonWithIgnoredPtr{}) - - desiredResult := ` - export class PersonWithIgnoredPtr { - name: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.name = source["name"]; - } - } -` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestMapWithPrefix(t *testing.T) { - t.Parallel() - - type Example struct { - Variable map[string]string `json:"variable"` - } - - converter := New().WithPrefix("prefix_").Add(Example{}) - - desiredResult := ` -export class prefix_Example { - variable: {[key: string]: string}; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.variable = source["variable"]; - } -} -` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestFieldNamesWithoutJSONAnnotation(t *testing.T) { - t.Parallel() - - type WithoutAnnotation struct { - PublicField string - privateField string - } - var tmp WithoutAnnotation - tmp.privateField = "" - - converter := New().Add(WithoutAnnotation{}) - desiredResult := ` -export class WithoutAnnotation { - PublicField: string; - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.PublicField = source["PublicField"]; - } -} -` - testConverter(t, converter, true, desiredResult, nil) -} - -func TestTypescriptifyComment(t *testing.T) { - t.Parallel() - type Person struct { - Age int `json:"age" ts_doc:"Age comment"` - Name string `json:"name" ts_doc:"Name comment"` - } - - converter := New() - - converter.AddType(reflect.TypeOf(Person{})) - converter.BackupDir = "" - converter.CreateConstructor = false - - desiredResult := `export class Person { - /** Age comment */ - age: number; - /** Name comment */ - name: string; -}` - testConverter(t, converter, false, desiredResult, nil) -} +package typescriptify + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "reflect" + "strings" + "testing" + "time" + + "github.com/fatih/structtag" + "github.com/stretchr/testify/assert" +) + +type Address struct { + // Used in html + Duration float64 `json:"duration"` + Text1 string `json:"text,omitempty"` + // Ignored: + Text2 string `json:",omitempty"` + Text3 string `json:"-"` +} + +type Dummy struct { + Something string `json:"something"` +} + +type HasName struct { + Name string `json:"name"` +} + +type Person struct { + HasName + Nicknames []string `json:"nicknames"` + Addresses []Address `json:"addresses"` + Address *Address `json:"address"` + Metadata string `json:"metadata" ts_type:"{[key:string]:string}" ts_transform:"JSON.parse(__VALUE__ || \"{}\")"` + Friends []*Person `json:"friends"` + Dummy Dummy `json:"a"` +} + +func TestTypescriptifyWithTypes(t *testing.T) { + t.Parallel() + converter := New() + + converter.AddType(reflect.TypeOf(Person{})) + converter.CreateConstructor = false + converter.BackupDir = "" + + desiredResult := `export class Dummy { + something: string; +} +export class Address { + duration: number; + text?: string; +} +export class Person { + name: string; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: {[key:string]:string}; + friends: Person[]; + a: Dummy; +}` + testConverter(t, converter, false, desiredResult, nil) +} + +func TestTypescriptifyWithCustomImports(t *testing.T) { + t.Parallel() + converter := New() + + converter.AddType(reflect.TypeOf(Person{})) + converter.BackupDir = "" + converter.AddImport("//import { Decimal } from 'decimal.js'") + converter.CreateConstructor = false + + desiredResult := ` +//import { Decimal } from 'decimal.js' + +export class Dummy { + something: string; +} +export class Address { + duration: number; + text?: string; +} +export class Person { + name: string; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: {[key:string]:string}; + friends: Person[]; + a: Dummy; +}` + testConverter(t, converter, false, desiredResult, nil) +} + +func TestTypescriptifyWithInstances(t *testing.T) { + t.Parallel() + converter := New() + + converter.Add(Person{}) + converter.Add(Dummy{}) + converter.DontExport = true + converter.BackupDir = "" + converter.CreateConstructor = false + + desiredResult := `class Dummy { + something: string; +} +class Address { + duration: number; + text?: string; +} +class Person { + name: string; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: {[key:string]:string}; + friends: Person[]; + a: Dummy; +}` + testConverter(t, converter, false, desiredResult, nil) +} + +func TestTypescriptifyWithInterfaces(t *testing.T) { + t.Parallel() + converter := New() + + converter.Add(Person{}) + converter.Add(Dummy{}) + converter.DontExport = true + converter.BackupDir = "" + converter.CreateInterface = true + + desiredResult := `interface Dummy { + something: string; +} +interface Address { + duration: number; + text?: string; +} +interface Person { + name: string; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: {[key:string]:string}; + friends: Person[]; + a: Dummy; +}` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestTypescriptifyWithAddTags(t *testing.T) { + t.Parallel() + converter := New() + + // tag all fields as optional (omitempty) + addressOptional := TagAll(reflect.TypeOf(Address{}), []string{"omitempty"}) + converter.BackupDir = "" + converter.CreateInterface = true + converter.ReadOnlyFields = true + converter.AddTypeWithName(addressOptional, "Address") + + desiredResult := `export interface Address { + readonly duration?: number; + readonly text?: string; +}` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestTypescriptifyWithFieldTags(t *testing.T) { + t.Parallel() + converter := New() + + // tag Address.Text1 with ts_type + // Union literals "domestic" | "european" | "international" + fieldTags := make(FieldTags) + fieldTags["Text1"] = []*structtag.Tag{ + { + Key: "ts_type", + Name: "\"domestic\" | \"european\" | \"international\"", + Options: []string{}, + }, + } + addressTagged := AddFieldTags(reflect.TypeOf(Address{}), &fieldTags) + converter.BackupDir = "" + converter.CreateInterface = true + converter.ReadOnlyFields = true + converter.AddTypeWithName(addressTagged, "Address") + + desiredResult := `export interface Address { + readonly duration: number; + readonly text?: "domestic" | "european" | "international"; +}` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestTypescriptifyWithDoubleClasses(t *testing.T) { + t.Parallel() + converter := New() + + converter.AddType(reflect.TypeOf(Person{})) + converter.AddType(reflect.TypeOf(Person{})) + converter.CreateConstructor = false + converter.BackupDir = "" + + desiredResult := `export class Dummy { + something: string; +} +export class Address { + duration: number; + text?: string; +} +export class Person { + name: string; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: {[key:string]:string}; + friends: Person[]; + a: Dummy; +}` + testConverter(t, converter, false, desiredResult, nil) +} + +func TestWithPrefixes(t *testing.T) { + t.Parallel() + converter := New() + + converter.Prefix = "test_" + converter.Suffix = "_test" + + converter.Add(Person{}) + converter.DontExport = true + converter.BackupDir = "" + + desiredResult := `class test_Dummy_test { + something: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.something = source["something"]; + } +} +class test_Address_test { + duration: number; + text?: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.duration = source["duration"]; + this.text = source["text"]; + } +} +class test_Person_test { + name: string; + nicknames: string[]; + addresses: test_Address_test[]; + address?: test_Address_test; + metadata: {[key:string]:string}; + friends: test_Person_test[]; + a: test_Dummy_test; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.nicknames = source["nicknames"]; + this.addresses = this.convertValues(source["addresses"], test_Address_test); + this.address = this.convertValues(source["address"], test_Address_test); + this.metadata = JSON.parse(source["metadata"] || "{}"); + this.friends = this.convertValues(source["friends"], test_Person_test); + this.a = this.convertValues(source["a"], test_Dummy_test); + } + + ` + tsConvertValuesFunc + ` +}` + jsn := jsonizeOrPanic(Person{ + Address: &Address{Text1: "txt1"}, + Addresses: []Address{{Text1: "111"}}, + Metadata: `{"something": "aaa"}`, + }) + testConverter(t, converter, true, desiredResult, []string{ + `new test_Person_test()`, + `JSON.stringify(new test_Person_test()?.metadata) === "{}"`, + `!(new test_Person_test()?.address)`, + `!(new test_Person_test()?.addresses)`, + `!(new test_Person_test()?.addresses)`, + + `new test_Person_test(` + jsn + ` as any)`, + `new test_Person_test(` + jsn + ` as any)?.metadata?.something === "aaa"`, + `(new test_Person_test(` + jsn + ` as any)?.address as test_Address_test).text === "txt1"`, + `new test_Person_test(` + jsn + ` as any)?.addresses?.length === 1`, + `(new test_Person_test(` + jsn + ` as any)?.addresses[0] as test_Address_test)?.text === "111"`, + }) +} + +func testConverter(t *testing.T, converter *TypeScriptify, strictMode bool, desiredResult string, tsExpressionAndDesiredResults []string) { + typeScriptCode, err := converter.Convert(nil) + if err != nil { + panic(err.Error()) + } + + fmt.Println("Desired: -------------------------------------------------------------------------------------------") + fmt.Println(desiredResult) + fmt.Println("Actual: -------------------------------------------------------------------------------------------") + fmt.Println(typeScriptCode) + fmt.Println("----------------------------------------------------------------------------------------------------") + + desiredResult = strings.TrimSpace(desiredResult) + typeScriptCode = strings.Trim(typeScriptCode, " \t\n\r") + if typeScriptCode != desiredResult { + gotLines1 := strings.Split(typeScriptCode, "\n") + expectedLines2 := strings.Split(desiredResult, "\n") + + max := len(gotLines1) + if len(expectedLines2) > max { + max = len(expectedLines2) + } + + for i := 0; i < max; i++ { + var gotLine, expectedLine string + if i < len(gotLines1) { + gotLine = gotLines1[i] + } + if i < len(expectedLines2) { + expectedLine = expectedLines2[i] + } + if assert.Equal(t, strings.TrimSpace(expectedLine), strings.TrimSpace(gotLine), "line #%d", 1+i) { + fmt.Printf("OK: %s\n", gotLine) + } else { + t.FailNow() + } + } + } + + if t.Failed() { + t.FailNow() + } + + testTypescriptExpression(t, strictMode, typeScriptCode, tsExpressionAndDesiredResults) +} + +func testTypescriptExpression(t *testing.T, strictMode bool, baseScript string, tsExpressionAndDesiredResults []string) { + f, err := os.CreateTemp(os.TempDir(), "*.ts") + assert.Nil(t, err) + assert.NotNil(t, f) + + if t.Failed() { + t.FailNow() + } + + _, _ = f.WriteString(baseScript) + _, _ = f.WriteString("\n") + for n, expr := range tsExpressionAndDesiredResults { + _, _ = f.WriteString("// " + expr + "\n") + _, _ = f.WriteString(`if (` + expr + `) { console.log("#` + fmt.Sprint(1+n) + ` OK") } else { throw new Error() }`) + _, _ = f.WriteString("\n\n") + } + + fmt.Println("tmp ts: ", f.Name()) + var byts []byte + if strictMode { + byts, err = exec.Command("npx", "tsc", "--strict", f.Name()).CombinedOutput() + } else { + byts, err = exec.Command("npx", "tsc", f.Name()).CombinedOutput() + } + assert.Nil(t, err, string(byts)) + + jsFile := strings.Replace(f.Name(), ".ts", ".js", 1) + fmt.Println("executing:", jsFile) + byts, err = exec.Command("node", jsFile).CombinedOutput() + assert.Nil(t, err, string(byts)) +} + +func TestTypescriptifyCustomType(t *testing.T) { + t.Parallel() + type TestCustomType struct { + Map map[string]int `json:"map" ts_type:"{[key: string]: number}"` + } + + converter := New() + + converter.AddType(reflect.TypeOf(TestCustomType{})) + converter.BackupDir = "" + converter.CreateConstructor = false + + desiredResult := `export class TestCustomType { + map: {[key: string]: number}; +}` + testConverter(t, converter, false, desiredResult, nil) +} + +func TestDate(t *testing.T) { + t.Parallel() + type TestCustomType struct { + Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"` + } + + converter := New() + converter.AddType(reflect.TypeOf(TestCustomType{})) + converter.BackupDir = "" + + desiredResult := `export class TestCustomType { + time: Date; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.time = new Date(source["time"]); + } +}` + + jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)}) + testConverter(t, converter, true, desiredResult, []string{ + `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`, + //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`, + `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`, + }) +} + +func TestDateWithoutTags(t *testing.T) { + t.Parallel() + type TestCustomType struct { + Time time.Time `json:"time"` + } + + // Test with custom field options defined per-one-struct: + converter1 := New() + converter1.Add(NewStruct(TestCustomType{}).WithFieldOpts(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"})) + converter1.BackupDir = "" + + // Test with custom field options defined globally: + converter2 := New() + converter2.Add(reflect.TypeOf(TestCustomType{})) + converter2.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"}) + converter2.BackupDir = "" + + for _, converter := range []*TypeScriptify{converter1, converter2} { + desiredResult := `export class TestCustomType { + time: Date; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.time = new Date(source["time"]); + } +}` + + jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)}) + testConverter(t, converter, true, desiredResult, []string{ + `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`, + //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`, + `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`, + }) + } +} + +func TestRecursive(t *testing.T) { + t.Parallel() + type Test struct { + Children []Test `json:"children"` + } + + converter := New() + + converter.AddType(reflect.TypeOf(Test{})) + converter.BackupDir = "" + + desiredResult := `export class Test { + children: Test[]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.children = this.convertValues(source["children"], Test); + } + + ` + tsConvertValuesFunc + ` +}` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestArrayOfArrays(t *testing.T) { + t.Parallel() + type Key struct { + Key string `json:"key"` + } + type Keyboard struct { + Keys [][]Key `json:"keys"` + } + + converter := New() + + converter.AddType(reflect.TypeOf(Keyboard{})) + converter.BackupDir = "" + + desiredResult := `export class Key { + key: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.key = source["key"]; + } +} +export class Keyboard { + keys: Key[][]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.keys = this.convertValues(source["keys"], Key); + } + + ` + tsConvertValuesFunc + ` +}` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestFixedArray(t *testing.T) { + t.Parallel() + type Sub struct{} + type Tmp struct { + Arr [3]string `json:"arr"` + Arr2 [3]Sub `json:"arr2"` + } + + converter := New() + + converter.AddType(reflect.TypeOf(Tmp{})) + converter.BackupDir = "" + + desiredResult := `export class Sub { + + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + + } +} +export class Tmp { + arr: string[]; + arr2: Sub[]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.arr = source["arr"]; + this.arr2 = this.convertValues(source["arr2"], Sub); + } + + ` + tsConvertValuesFunc + ` +} +` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestAny(t *testing.T) { + t.Parallel() + type Test struct { + Any interface{} `json:"field"` + } + + converter := New() + + converter.AddType(reflect.TypeOf(Test{})) + converter.BackupDir = "" + + desiredResult := `export class Test { + field: any; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.field = source["field"]; + } +}` + testConverter(t, converter, true, desiredResult, nil) +} + +type NumberTime time.Time + +func (t NumberTime) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("%d", time.Time(t).Unix())), nil +} + +func TestTypeAlias(t *testing.T) { + t.Parallel() + type Person struct { + Birth NumberTime `json:"birth" ts_type:"number"` + } + + converter := New() + + converter.AddType(reflect.TypeOf(Person{})) + converter.BackupDir = "" + converter.CreateConstructor = false + + desiredResult := `export class Person { + birth: number; +}` + testConverter(t, converter, false, desiredResult, nil) +} + +type MSTime struct { + time.Time +} + +func (MSTime) UnmarshalJSON([]byte) error { return nil } +func (MSTime) MarshalJSON() ([]byte, error) { return []byte("1111"), nil } + +func TestOverrideCustomType(t *testing.T) { + t.Parallel() + + type SomeStruct struct { + Time MSTime `json:"time" ts_type:"number"` + } + var _ json.Marshaler = new(MSTime) + var _ json.Unmarshaler = new(MSTime) + + converter := New() + + converter.AddType(reflect.TypeOf(SomeStruct{})) + converter.BackupDir = "" + converter.CreateConstructor = false + + desiredResult := `export class SomeStruct { + time: number; +}` + testConverter(t, converter, false, desiredResult, nil) + + byts, _ := json.Marshal(SomeStruct{Time: MSTime{Time: time.Now()}}) + assert.Equal(t, `{"time":1111}`, string(byts)) +} + +type Weekday int + +const ( + Sunday Weekday = iota + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday +) + +func (w Weekday) TSName() string { + switch w { + case Sunday: + return "SUNDAY" + case Monday: + return "MONDAY" + case Tuesday: + return "TUESDAY" + case Wednesday: + return "WEDNESDAY" + case Thursday: + return "THURSDAY" + case Friday: + return "FRIDAY" + case Saturday: + return "SATURDAY" + default: + return "???" + } +} + +// One way to specify enums is to list all values and then every one must have a TSName() method +var allWeekdaysV1 = []Weekday{ + Sunday, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, +} + +// Another way to specify enums: +var allWeekdaysV2 = []struct { + Value Weekday + TSName string +}{ + {Sunday, "SUNDAY"}, + {Monday, "MONDAY"}, + {Tuesday, "TUESDAY"}, + {Wednesday, "WEDNESDAY"}, + {Thursday, "THURSDAY"}, + {Friday, "FRIDAY"}, + {Saturday, "SATURDAY"}, +} + +type Holliday struct { + Name string `json:"name"` + Weekday Weekday `json:"weekday"` +} + +func TestEnum(t *testing.T) { + t.Parallel() + for _, allWeekdays := range []interface{}{allWeekdaysV1, allWeekdaysV2} { + converter := New(). + AddType(reflect.TypeOf(Holliday{})). + AddEnum(allWeekdays). + WithConstructor(true). + WithBackupDir("") + + desiredResult := `export enum Weekday { + SUNDAY = 0, + MONDAY = 1, + TUESDAY = 2, + WEDNESDAY = 3, + THURSDAY = 4, + FRIDAY = 5, + SATURDAY = 6, +} +export class Holliday { + name: string; + weekday: Weekday; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.weekday = source["weekday"]; + } +}` + testConverter(t, converter, true, desiredResult, nil) + } +} + +type Gender string + +const ( + MaleStr Gender = "m" + FemaleStr Gender = "f" +) + +var allGenders = []struct { + Value Gender + TSName string +}{ + {MaleStr, "MALE"}, + {FemaleStr, "FEMALE"}, +} + +func TestEnumWithStringValues(t *testing.T) { + t.Parallel() + converter := New(). + AddEnum(allGenders). + WithConstructor(false). + WithBackupDir("") + + desiredResult := ` +export enum Gender { + MALE = "m", + FEMALE = "f", +} +` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestConstructorWithReferences(t *testing.T) { + t.Parallel() + converter := New(). + AddType(reflect.TypeOf(Person{})). + AddEnum(allWeekdaysV2). + WithConstructor(true). + WithBackupDir("") + + desiredResult := `export enum Weekday { + SUNDAY = 0, + MONDAY = 1, + TUESDAY = 2, + WEDNESDAY = 3, + THURSDAY = 4, + FRIDAY = 5, + SATURDAY = 6, +} +export class Dummy { + something: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.something = source["something"]; + } +} +export class Address { + duration: number; + text?: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.duration = source["duration"]; + this.text = source["text"]; + } +} +export class Person { + name: string; + nicknames: string[]; + addresses: Address[]; + address?: Address; + metadata: {[key:string]:string}; + friends: Person[]; + a: Dummy; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.nicknames = source["nicknames"]; + this.addresses = this.convertValues(source["addresses"], Address); + this.address = this.convertValues(source["address"], Address); + this.metadata = JSON.parse(source["metadata"] || "{}"); + this.friends = this.convertValues(source["friends"], Person); + this.a = this.convertValues(source["a"], Dummy); + } + + ` + tsConvertValuesFunc + ` +}` + testConverter(t, converter, true, desiredResult, nil) +} + +type WithMap struct { + Map map[string]int `json:"simpleMap"` + MapObjects map[string]Address `json:"mapObjects"` + PtrMap *map[string]Address `json:"ptrMapObjects"` +} + +func TestMaps(t *testing.T) { + t.Parallel() + converter := New(). + AddType(reflect.TypeOf(WithMap{})). + WithConstructor(true). + WithPrefix("API_"). + WithBackupDir("") + + desiredResult := ` + export class API_Address { + duration: number; + text?: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.duration = source["duration"]; + this.text = source["text"]; + } + } + export class API_WithMap { + simpleMap: {[key: string]: number}; + mapObjects: {[key: string]: API_Address}; + ptrMapObjects?: {[key: string]: API_Address}; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.simpleMap = source["simpleMap"]; + this.mapObjects = this.convertValues(source["mapObjects"], API_Address, true); + this.ptrMapObjects = this.convertValues(source["ptrMapObjects"], API_Address, true); + } + + ` + tsConvertValuesFunc + ` + } +` + + json := WithMap{ + Map: map[string]int{"aaa": 1}, + MapObjects: map[string]Address{"bbb": {Duration: 1.0, Text1: "txt1"}}, + PtrMap: &map[string]Address{"ccc": {Duration: 2.0, Text1: "txt2"}}, + } + + testConverter(t, converter, true, desiredResult, []string{ + `new API_WithMap(` + jsonizeOrPanic(json) + `).simpleMap.aaa == 1`, + `(new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_Address`, + `!((new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_WithMap)`, + `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.duration == 1`, + `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.text === "txt1"`, + `(new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_Address`, + `!((new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_WithMap)`, + `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.duration === 2`, + `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.text === "txt2"`, + }) +} + +func TestPTR(t *testing.T) { + t.Parallel() + type Person struct { + Name *string `json:"name"` + } + + converter := New() + converter.BackupDir = "" + converter.CreateConstructor = false + converter.Add(Person{}) + + desiredResult := `export class Person { + name?: string; +}` + testConverter(t, converter, true, desiredResult, nil) +} + +type PersonWithPtrName struct { + *HasName +} + +func TestAnonymousPtr(t *testing.T) { + t.Parallel() + var p PersonWithPtrName + p.HasName = &HasName{} + p.Name = "JKLJKL" + converter := New(). + AddType(reflect.TypeOf(PersonWithPtrName{})). + WithConstructor(true). + WithBackupDir("") + + desiredResult := ` + export class PersonWithPtrName { + name: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } +` + testConverter(t, converter, true, desiredResult, nil) +} + +func jsonizeOrPanic(i interface{}) string { + byts, err := json.Marshal(i) + if err != nil { + panic(err) + } + return string(byts) +} + +func TestTestConverter(t *testing.T) { + t.Parallel() + + ts := `class Converter { + ` + tsConvertValuesFunc + ` +} +const converter = new Converter(); + +class Address { + street: string; + number: number; + + constructor(a: any) { + this.street = a["street"]; + this.number = a["number"]; + } +} +` + + testTypescriptExpression(t, true, ts, []string{ + `(converter.convertValues(null, Address)) === null`, + `(converter.convertValues([], Address)).length === 0`, + `(converter.convertValues({}, Address)) instanceof Address`, + `!(converter.convertValues({}, Address, true) instanceof Address)`, + + `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[]).length == 1`, + `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0] instanceof Address`, + `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].number === 19`, + `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].street === "aaa"`, + + `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[]).length == 1`, + `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0] instanceof Address`, + `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].number === 19`, + `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].street === "aaa"`, + + `Object.keys((converter.convertValues({"first": {street: "aaa", number: 19}}, Address, true) as {[_: string]: Address})).length == 1`, + `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"] instanceof Address`, + `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].number === 19`, + `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].street === "aaa"`, + }) +} + +func TestIgnoredPTR(t *testing.T) { + t.Parallel() + type PersonWithIgnoredPtr struct { + Name string `json:"name"` + Nickname *string `json:"-"` + } + + converter := New() + converter.BackupDir = "" + converter.Add(PersonWithIgnoredPtr{}) + + desiredResult := ` + export class PersonWithIgnoredPtr { + name: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } +` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestMapWithPrefix(t *testing.T) { + t.Parallel() + + type Example struct { + Variable map[string]string `json:"variable"` + } + + converter := New().WithPrefix("prefix_").Add(Example{}) + + desiredResult := ` +export class prefix_Example { + variable: {[key: string]: string}; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.variable = source["variable"]; + } +} +` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestFieldNamesWithoutJSONAnnotation(t *testing.T) { + t.Parallel() + + type WithoutAnnotation struct { + PublicField string + privateField string + } + var tmp WithoutAnnotation + tmp.privateField = "" + + converter := New().Add(WithoutAnnotation{}) + desiredResult := ` +export class WithoutAnnotation { + PublicField: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.PublicField = source["PublicField"]; + } +} +` + testConverter(t, converter, true, desiredResult, nil) +} + +func TestTypescriptifyComment(t *testing.T) { + t.Parallel() + type Person struct { + Age int `json:"age" ts_doc:"Age comment"` + Name string `json:"name" ts_doc:"Name comment"` + } + + converter := New() + + converter.AddType(reflect.TypeOf(Person{})) + converter.BackupDir = "" + converter.CreateConstructor = false + + desiredResult := `export class Person { + /** Age comment */ + age: number; + /** Name comment */ + name: string; +}` + testConverter(t, converter, false, desiredResult, nil) +}