├── .gitignore
├── .editorconfig
├── go.mod
├── typescriptify
├── utils.go
├── typescriptify.go
└── typescriptify_test.go
├── scripts
└── json_to_ts.sh
├── browser_test
├── example_output_interfaces.ts
├── test.html
├── example_output.ts
└── example_output.js
├── makefile
├── example
├── example-models
│ └── example_models.go
└── example.go
├── .github
└── workflows
│ └── go.yml
├── go.sum
├── CHANGELOG.md
├── tscriptify
└── main.go
├── README.md
└── LICENSE.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | *.iml
4 | tags
5 | tmp_*
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = crlf
6 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tkrajina/typescriptify-golang-structs
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/stretchr/testify v1.7.0
7 | github.com/tkrajina/go-reflector v0.5.5
8 | )
9 |
--------------------------------------------------------------------------------
/typescriptify/utils.go:
--------------------------------------------------------------------------------
1 | package typescriptify
2 |
3 | import "strings"
4 |
5 | func indentLines(str string, i int) string {
6 | lines := strings.Split(str, "\n")
7 | for n := range lines {
8 | lines[n] = strings.Repeat("\t", i) + lines[n]
9 | }
10 | return strings.Join(lines, "\n")
11 | }
12 |
--------------------------------------------------------------------------------
/scripts/json_to_ts.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | json=$1
5 | model_name=$2
6 |
7 | if [ -z "$json" ];
8 | then
9 | echo "No JSON specified"
10 | exit 1
11 | fi
12 | if [ -z "$model_name" ];
13 | then
14 | echo "No model name specified"
15 | exit 1
16 | fi
17 |
18 | mkdir -p $GOPATH/src/tmp_models
19 | echo "Using $GOPATH/src/tmp_models ad temporary models package"
20 | cat $model_name.json | gojson -pkg tmp_models -name=$model_name > $GOPATH/src/tmp_models/$model_name.go
21 | tscriptify -package=tmp_models -target $model_name.ts $model_name
22 | echo "Saved to $model_name"
23 |
--------------------------------------------------------------------------------
/browser_test/example_output_interfaces.ts:
--------------------------------------------------------------------------------
1 | /* Do not change, this code is generated from Golang structs */
2 |
3 |
4 | export interface Address {
5 | city: string;
6 | number: number;
7 | country?: string;
8 | }
9 | export interface PersonalInfo {
10 | hobby: string[];
11 | pet_name: string;
12 | }
13 | export interface Person {
14 | name: string;
15 | personal_info: PersonalInfo;
16 | nicknames: string[];
17 | addresses: Address[];
18 | address?: Address;
19 | metadata: {[key:string]:string};
20 | friends: Person[];
21 | //[Person:]
22 | /* Custom code here */
23 |
24 | [key: string]: any
25 |
26 | //[end]
27 | }
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 | build:
3 | go build -i -v -o /dev/null ./...
4 |
5 | .PHONY: install
6 | install:
7 | go install ./...
8 |
9 | .PHONY: test
10 | test: lint
11 | go test ./...
12 | go run example/example.go
13 | tsc browser_test/example_output.ts
14 | # Make sure dommandline tool works:
15 | go run tscriptify/main.go -package github.com/tkrajina/typescriptify-golang-structs/example/example-models -verbose -target tmp_classes.ts example/example-models/example_models.go
16 | go run tscriptify/main.go -package github.com/tkrajina/typescriptify-golang-structs/example/example-models -verbose -target tmp_interfaces.ts -interface example/example-models/example_models.go
17 |
18 | .PHONY: lint
19 | lint:
20 | go vet ./...
21 | -golangci-lint run
22 |
--------------------------------------------------------------------------------
/example/example-models/example_models.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Address struct {
4 | // Used in html
5 | City string `json:"city"`
6 | Number float64 `json:"number"`
7 | Country string `json:"country,omitempty"`
8 | }
9 |
10 | type PersonalInfo struct {
11 | Hobbies []string `json:"hobby"`
12 | PetName string `json:"pet_name"`
13 | }
14 |
15 | type Person struct {
16 | Name string `json:"name"`
17 | PersonalInfo PersonalInfo `json:"personal_info"`
18 | Nicknames []string `json:"nicknames"`
19 | Addresses []Address `json:"addresses"`
20 | Address *Address `json:"address"`
21 | Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"`
22 | Friends []*Person `json:"friends"`
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: '*'
6 | pull_request:
7 | branches: 'master'
8 |
9 | jobs:
10 | build:
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | go-version: [1.16.x]
15 | os: [ubuntu-latest, macos-latest, windows-latest]
16 | name: Build and test
17 | runs-on: ${{ matrix.os }}
18 | steps:
19 |
20 | - name: Set up Go 1.x
21 | uses: actions/setup-go@v2
22 | with:
23 | go-version: ${{ matrix.go-version }}
24 | id: go
25 |
26 | - name: Check out code into the Go module directory
27 | uses: actions/checkout@v2
28 |
29 | - name: Get dependencies
30 | run: |
31 | go mod download
32 |
33 | - name: Build
34 | run: |
35 | cd tscriptify
36 | go build -v .
37 |
38 | - name: Test
39 | run: |
40 | cd typescriptify
41 | go test -v .
42 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8 | github.com/tkrajina/go-reflector v0.5.4 h1:dS9aJEa/eYNQU/fwsb5CSiATOxcNyA/gG/A7a582D5s=
9 | github.com/tkrajina/go-reflector v0.5.4/go.mod h1:9PyLgEOzc78ey/JmQQHbW8cQJ1oucLlNQsg8yFvkVk8=
10 | github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ=
11 | github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 |
--------------------------------------------------------------------------------
/example/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/tkrajina/typescriptify-golang-structs/typescriptify"
4 |
5 | type Address struct {
6 | // Used in html
7 | City string `json:"city"`
8 | Number float64 `json:"number"`
9 | Country string `json:"country,omitempty"`
10 | }
11 |
12 | type PersonalInfo struct {
13 | Hobbies []string `json:"hobby"`
14 | PetName string `json:"pet_name"`
15 | }
16 |
17 | type Person struct {
18 | Name string `json:"name"`
19 | PersonalInfo PersonalInfo `json:"personal_info"`
20 | Nicknames []string `json:"nicknames"`
21 | Addresses []Address `json:"addresses"`
22 | Address *Address `json:"address"`
23 | Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"`
24 | Friends []*Person `json:"friends"`
25 | }
26 |
27 | func main() {
28 | converter := typescriptify.New()
29 | converter.CreateConstructor = true
30 | converter.Indent = " "
31 | converter.BackupDir = ""
32 |
33 | converter.Add(Person{})
34 |
35 | err := converter.ConvertToFile("browser_test/example_output.ts")
36 | if err != nil {
37 | panic(err.Error())
38 | }
39 |
40 | converter.CreateInterface = true
41 | err = converter.ConvertToFile("browser_test/example_output_interfaces.ts")
42 | if err != nil {
43 | panic(err.Error())
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.1.10
4 |
5 | - missing prefix in struct map value types
6 |
7 | ## v0.1.8, v0.1.9
8 |
9 | - Typescript doc tags
10 | - Handle fields that is not annotated with json tag
11 | - Matrix testing
12 |
13 | ## v0.1.7
14 |
15 | - Handle packages with hyphens
16 |
17 | ## v0.1.6
18 |
19 | - Fix map keys if suffix/prefix specified
20 | - process customImports on Params with other flags
21 |
22 | ## v0.1.5
23 |
24 | - Fixed panic with arrays
25 | - Use go modules for dependency management
26 | - Example shell script how to create a typescript model directly from json
27 |
28 | ## v0.1.4
29 |
30 | - fix ignored pointers
31 | - interface cmdline flag
32 |
33 | ## v0.1.2
34 |
35 | - Log field and type creation to make the order (and why a type was converted) simpler to follow
36 | - Global custom types: Merge branch 'fix-33' of https://github.com/shackra/typescriptify-golang-structs into shackra-fix-33
37 |
38 | ## v0.1.1
39 |
40 | - custom types (insted of setting `ts_type` and `ts_transform` every time)
41 |
42 | ## v0.1.0
43 |
44 | - simplified conversion of objects
45 | - Pointer anonymous structs
46 | - more (and better) tests
47 | - maps of objects
48 | - convert in constructors (createFrom deprecated)
49 | - custom imports
50 | - Add `?` to field name if it's a pointer type
51 | - New way of defining enums
52 |
--------------------------------------------------------------------------------
/browser_test/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test
4 |
5 |
57 |
58 |
59 | Test
60 | OK (check browser console for errors)?
61 |
62 |
63 |
--------------------------------------------------------------------------------
/browser_test/example_output.ts:
--------------------------------------------------------------------------------
1 | /* Do not change, this code is generated from Golang structs */
2 |
3 |
4 | export class Address {
5 | city: string;
6 | number: number;
7 | country?: string;
8 |
9 | constructor(source: any = {}) {
10 | if ('string' === typeof source) source = JSON.parse(source);
11 | this.city = source["city"];
12 | this.number = source["number"];
13 | this.country = source["country"];
14 | }
15 | //[Address:]
16 | /* Custom code here */
17 |
18 | getAddressString = () => {
19 | return this.city + " " + this.number;
20 | }
21 |
22 | //[end]
23 | }
24 | export class PersonalInfo {
25 | hobby: string[];
26 | pet_name: string;
27 |
28 | constructor(source: any = {}) {
29 | if ('string' === typeof source) source = JSON.parse(source);
30 | this.hobby = source["hobby"];
31 | this.pet_name = source["pet_name"];
32 | }
33 | //[PersonalInfo:]
34 |
35 | getPersonalInfoString = () => {
36 | return "pet:" + this.pet_name;
37 | }
38 |
39 | //[end]
40 | }
41 | export class Person {
42 | name: string;
43 | personal_info: PersonalInfo;
44 | nicknames: string[];
45 | addresses: Address[];
46 | address?: Address;
47 | metadata: {[key:string]:string};
48 | friends: Person[];
49 |
50 | constructor(source: any = {}) {
51 | if ('string' === typeof source) source = JSON.parse(source);
52 | this.name = source["name"];
53 | this.personal_info = this.convertValues(source["personal_info"], PersonalInfo);
54 | this.nicknames = source["nicknames"];
55 | this.addresses = this.convertValues(source["addresses"], Address);
56 | this.address = this.convertValues(source["address"], Address);
57 | this.metadata = source["metadata"];
58 | this.friends = this.convertValues(source["friends"], Person);
59 | }
60 |
61 | convertValues(a: any, classs: any, asMap: boolean = false): any {
62 | if (!a) {
63 | return a;
64 | }
65 | if (Array.isArray(a)) {
66 | return (a as any[]).map(elem => this.convertValues(elem, classs));
67 | } else if ("object" === typeof a) {
68 | if (asMap) {
69 | for (const key of Object.keys(a)) {
70 | a[key] = new classs(a[key]);
71 | }
72 | return a;
73 | }
74 | return new classs(a);
75 | }
76 | return a;
77 | }
78 | //[Person:]
79 |
80 | getInfo = () => {
81 | return "name:" + this.name;
82 | }
83 |
84 | //[end]
85 | }
--------------------------------------------------------------------------------
/browser_test/example_output.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* Do not change, this code is generated from Golang structs */
3 | Object.defineProperty(exports, "__esModule", { value: true });
4 | exports.Person = exports.PersonalInfo = exports.Address = void 0;
5 | var Address = /** @class */ (function () {
6 | function Address(source) {
7 | if (source === void 0) { source = {}; }
8 | var _this = this;
9 | //[Address:]
10 | /* Custom code here */
11 | this.getAddressString = function () {
12 | return _this.city + " " + _this.number;
13 | };
14 | if ('string' === typeof source)
15 | source = JSON.parse(source);
16 | this.city = source["city"];
17 | this.number = source["number"];
18 | this.country = source["country"];
19 | }
20 | return Address;
21 | }());
22 | exports.Address = Address;
23 | var PersonalInfo = /** @class */ (function () {
24 | function PersonalInfo(source) {
25 | if (source === void 0) { source = {}; }
26 | var _this = this;
27 | //[PersonalInfo:]
28 | this.getPersonalInfoString = function () {
29 | return "pet:" + _this.pet_name;
30 | };
31 | if ('string' === typeof source)
32 | source = JSON.parse(source);
33 | this.hobby = source["hobby"];
34 | this.pet_name = source["pet_name"];
35 | }
36 | return PersonalInfo;
37 | }());
38 | exports.PersonalInfo = PersonalInfo;
39 | var Person = /** @class */ (function () {
40 | function Person(source) {
41 | if (source === void 0) { source = {}; }
42 | var _this = this;
43 | //[Person:]
44 | this.getInfo = function () {
45 | return "name:" + _this.name;
46 | };
47 | if ('string' === typeof source)
48 | source = JSON.parse(source);
49 | this.name = source["name"];
50 | this.personal_info = this.convertValues(source["personal_info"], PersonalInfo);
51 | this.nicknames = source["nicknames"];
52 | this.addresses = this.convertValues(source["addresses"], Address);
53 | this.address = this.convertValues(source["address"], Address);
54 | this.metadata = source["metadata"];
55 | this.friends = this.convertValues(source["friends"], Person);
56 | }
57 | Person.prototype.convertValues = function (a, classs, asMap) {
58 | var _this = this;
59 | if (asMap === void 0) { asMap = false; }
60 | if (!a) {
61 | return a;
62 | }
63 | if (Array.isArray(a)) {
64 | return a.map(function (elem) { return _this.convertValues(elem, classs); });
65 | }
66 | else if ("object" === typeof a) {
67 | if (asMap) {
68 | for (var _i = 0, _a = Object.keys(a); _i < _a.length; _i++) {
69 | var key = _a[_i];
70 | a[key] = new classs(a[key]);
71 | }
72 | return a;
73 | }
74 | return new classs(a);
75 | }
76 | return a;
77 | };
78 | return Person;
79 | }());
80 | exports.Person = Person;
81 |
--------------------------------------------------------------------------------
/tscriptify/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "go/ast"
7 | "go/parser"
8 | "go/token"
9 | "os"
10 | "os/exec"
11 | "strings"
12 | "text/template"
13 | )
14 |
15 | type arrayImports []string
16 |
17 | func (i *arrayImports) String() string {
18 | return "// custom imports:\n\n" + strings.Join(*i, "\n")
19 | }
20 |
21 | func (i *arrayImports) Set(value string) error {
22 | *i = append(*i, value)
23 | return nil
24 | }
25 |
26 | const TEMPLATE = `package main
27 |
28 | import (
29 | "fmt"
30 |
31 | m "{{ .ModelsPackage }}"
32 | "github.com/tkrajina/typescriptify-golang-structs/typescriptify"
33 | )
34 |
35 | func main() {
36 | t := typescriptify.New()
37 | t.CreateInterface = {{ .Interface }}
38 | {{ range $key, $value := .InitParams }} t.{{ $key }}={{ $value }}
39 | {{ end }}
40 | {{ range .Structs }} t.Add({{ . }}{})
41 | {{ end }}
42 | {{ range .CustomImports }} t.AddImport("{{ . }}")
43 | {{ end }}
44 | err := t.ConvertToFile("{{ .TargetFile }}")
45 | if err != nil {
46 | panic(err.Error())
47 | }
48 | fmt.Println("OK")
49 | }`
50 |
51 | type Params struct {
52 | ModelsPackage string
53 | TargetFile string
54 | Structs []string
55 | InitParams map[string]interface{}
56 | CustomImports arrayImports
57 | Interface bool
58 | Verbose bool
59 | }
60 |
61 | func main() {
62 | var p Params
63 | var backupDir string
64 | flag.StringVar(&p.ModelsPackage, "package", "", "Path of the package with models")
65 | flag.StringVar(&p.TargetFile, "target", "", "Target typescript file")
66 | flag.StringVar(&backupDir, "backup", "", "Directory where backup files are saved")
67 | flag.BoolVar(&p.Interface, "interface", false, "Create interfaces (not classes)")
68 | flag.Var(&p.CustomImports, "import", "Typescript import for your custom type, repeat this option for each import needed")
69 | flag.BoolVar(&p.Verbose, "verbose", false, "Verbose logs")
70 | flag.Parse()
71 |
72 | structs := []string{}
73 | for _, structOrGoFile := range flag.Args() {
74 | if strings.HasSuffix(structOrGoFile, ".go") {
75 | fmt.Println("Parsing:", structOrGoFile)
76 | fileStructs, err := GetGolangFileStructs(structOrGoFile)
77 | if err != nil {
78 | panic(fmt.Sprintf("Error loading/parsing golang file %s: %s", structOrGoFile, err.Error()))
79 | }
80 | structs = append(structs, fileStructs...)
81 | } else {
82 | structs = append(structs, structOrGoFile)
83 | }
84 | }
85 |
86 | if len(p.ModelsPackage) == 0 {
87 | fmt.Fprintln(os.Stderr, "No package given")
88 | os.Exit(1)
89 | }
90 | if len(p.TargetFile) == 0 {
91 | fmt.Fprintln(os.Stderr, "No target file")
92 | os.Exit(1)
93 | }
94 |
95 | t := template.Must(template.New("").Parse(TEMPLATE))
96 |
97 | f, err := os.CreateTemp(os.TempDir(), "typescriptify_*.go")
98 | handleErr(err)
99 | defer f.Close()
100 |
101 | structsArr := make([]string, 0)
102 | for _, str := range structs {
103 | str = strings.TrimSpace(str)
104 | if len(str) > 0 {
105 | structsArr = append(structsArr, "m."+str)
106 | }
107 | }
108 |
109 | p.Structs = structsArr
110 | p.InitParams = map[string]interface{}{
111 | "BackupDir": fmt.Sprintf(`"%s"`, backupDir),
112 | }
113 | err = t.Execute(f, p)
114 | handleErr(err)
115 |
116 | if p.Verbose {
117 | byts, err := os.ReadFile(f.Name())
118 | handleErr(err)
119 | fmt.Printf("\nCompiling generated code (%s):\n%s\n----------------------------------------------------------------------------------------------------\n", f.Name(), string(byts))
120 | }
121 |
122 | cmd := exec.Command("go", "run", f.Name())
123 | fmt.Println(strings.Join(cmd.Args, " "))
124 | output, err := cmd.CombinedOutput()
125 | if err != nil {
126 | fmt.Println(string(output))
127 | handleErr(err)
128 | }
129 | fmt.Println(string(output))
130 | }
131 |
132 | func GetGolangFileStructs(filename string) ([]string, error) {
133 | fset := token.NewFileSet() // positions are relative to fset
134 |
135 | f, err := parser.ParseFile(fset, filename, nil, 0)
136 | if err != nil {
137 | return nil, err
138 | }
139 |
140 | v := &AVisitor{}
141 | ast.Walk(v, f)
142 |
143 | return v.structs, nil
144 | }
145 |
146 | type AVisitor struct {
147 | structNameCandidate string
148 | structs []string
149 | }
150 |
151 | func (v *AVisitor) Visit(node ast.Node) ast.Visitor {
152 | if node != nil {
153 | switch t := node.(type) {
154 | case *ast.Ident:
155 | v.structNameCandidate = t.Name
156 | case *ast.StructType:
157 | if len(v.structNameCandidate) > 0 {
158 | v.structs = append(v.structs, v.structNameCandidate)
159 | v.structNameCandidate = ""
160 | }
161 | default:
162 | v.structNameCandidate = ""
163 | }
164 | }
165 | return v
166 | }
167 |
168 | func handleErr(err error) {
169 | if err != nil {
170 | panic(err.Error())
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A Golang JSON to TypeScript model converter
2 |
3 | ## Installation
4 |
5 | The command-line tool:
6 |
7 | ```
8 | go install github.com/tkrajina/typescriptify-golang-structs/tscriptify
9 | ```
10 |
11 | The library:
12 |
13 | ```
14 | go get github.com/tkrajina/typescriptify-golang-structs
15 | ```
16 |
17 | ## Usage
18 |
19 | Use the command line tool:
20 |
21 | ```
22 | tscriptify -package=package/with/your/models -target=target_ts_file.ts Model1 Model2
23 | ```
24 |
25 | If you need to import a custom type in Typescript, you can pass the import string:
26 |
27 | ```
28 | tscriptify -package=package/with/your/models -target=target_ts_file.ts -import="import { Decimal } from 'decimal.js'" Model1 Model2
29 | ```
30 |
31 | If all your structs are in one file, you can convert them with:
32 |
33 | ```
34 | tscriptify -package=package/with/your/models -target=target_ts_file.ts path/to/file/with/structs.go
35 | ```
36 |
37 | Or by using it from your code:
38 |
39 | ```golang
40 | converter := typescriptify.New().
41 | Add(Person{}).
42 | Add(Dummy{})
43 | err := converter.ConvertToFile("ts/models.ts")
44 | if err != nil {
45 | panic(err.Error())
46 | }
47 | ```
48 |
49 | Command line options:
50 |
51 | ```
52 | $ tscriptify --help
53 | Usage of tscriptify:
54 | -backup string
55 | Directory where backup files are saved
56 | -package string
57 | Path of the package with models
58 | -target string
59 | Target typescript file
60 | ```
61 |
62 | ## Models and conversion
63 |
64 | If the `Person` structs contain a reference to the `Address` struct, then you don't have to add `Address` explicitly. Only fields with a valid `json` tag will be converted to TypeScript models.
65 |
66 | Example input structs:
67 |
68 | ```golang
69 | type Address struct {
70 | City string `json:"city"`
71 | Number float64 `json:"number"`
72 | Country string `json:"country,omitempty"`
73 | }
74 |
75 | type PersonalInfo struct {
76 | Hobbies []string `json:"hobby"`
77 | PetName string `json:"pet_name"`
78 | }
79 |
80 | type Person struct {
81 | Name string `json:"name"`
82 | PersonalInfo PersonalInfo `json:"personal_info"`
83 | Nicknames []string `json:"nicknames"`
84 | Addresses []Address `json:"addresses"`
85 | Address *Address `json:"address"`
86 | Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"`
87 | Friends []*Person `json:"friends"`
88 | }
89 | ```
90 |
91 | Generated TypeScript:
92 |
93 | ```typescript
94 | export class Address {
95 | city: string;
96 | number: number;
97 | country?: string;
98 |
99 | constructor(source: any = {}) {
100 | if ('string' === typeof source) source = JSON.parse(source);
101 | this.city = source["city"];
102 | this.number = source["number"];
103 | this.country = source["country"];
104 | }
105 | }
106 | export class PersonalInfo {
107 | hobby: string[];
108 | pet_name: string;
109 |
110 | constructor(source: any = {}) {
111 | if ('string' === typeof source) source = JSON.parse(source);
112 | this.hobby = source["hobby"];
113 | this.pet_name = source["pet_name"];
114 | }
115 | }
116 | export class Person {
117 | name: string;
118 | personal_info: PersonalInfo;
119 | nicknames: string[];
120 | addresses: Address[];
121 | address?: Address;
122 | metadata: {[key:string]:string};
123 | friends: Person[];
124 |
125 | constructor(source: any = {}) {
126 | if ('string' === typeof source) source = JSON.parse(source);
127 | this.name = source["name"];
128 | this.personal_info = this.convertValues(source["personal_info"], PersonalInfo);
129 | this.nicknames = source["nicknames"];
130 | this.addresses = this.convertValues(source["addresses"], Address);
131 | this.address = this.convertValues(source["address"], Address);
132 | this.metadata = source["metadata"];
133 | this.friends = this.convertValues(source["friends"], Person);
134 | }
135 |
136 | convertValues(a: any, classs: any, asMap: boolean = false): any {
137 | if (!a) {
138 | return a;
139 | }
140 | if (a.slice) {
141 | return (a as any[]).map(elem => this.convertValues(elem, classs));
142 | } else if ("object" === typeof a) {
143 | if (asMap) {
144 | for (const key of Object.keys(a)) {
145 | a[key] = new classs(a[key]);
146 | }
147 | return a;
148 | }
149 | return new classs(a);
150 | }
151 | return a;
152 | }
153 | }
154 | ```
155 |
156 | If you prefer interfaces, the output is:
157 |
158 | ```typescript
159 | export interface Address {
160 | city: string;
161 | number: number;
162 | country?: string;
163 | }
164 | export interface PersonalInfo {
165 | hobby: string[];
166 | pet_name: string;
167 | }
168 | export interface Person {
169 | name: string;
170 | personal_info: PersonalInfo;
171 | nicknames: string[];
172 | addresses: Address[];
173 | address?: Address;
174 | metadata: {[key:string]:string};
175 | friends: Person[];
176 | }
177 | ```
178 |
179 | In TypeScript you can just cast your json object in any of those models:
180 |
181 | ```typescript
182 | var person = {"name":"Me myself","nicknames":["aaa", "bbb"]};
183 | console.log(person.name);
184 | // The TypeScript compiler will throw an error for this line
185 | console.log(person.something);
186 | ```
187 |
188 | ## Custom Typescript code
189 |
190 | Any custom code can be added to Typescript models:
191 |
192 | ```typescript
193 | class Address {
194 | street : string;
195 | no : number;
196 | //[Address:]
197 | country: string;
198 | getStreetAndNumber() {
199 | return street + " " + number;
200 | }
201 | //[end]
202 | }
203 | ```
204 |
205 | The lines between `//[Address:]` and `//[end]` will be left intact after `ConvertToFile()`.
206 |
207 | If your custom code contain methods, then just casting yout object to the target class (with ` {...}`) won't work because the casted object won't contain your methods.
208 |
209 | In that case use the constructor:
210 |
211 | ```typescript
212 | var person = new Person({"name":"Me myself","nicknames":["aaa", "bbb"]});
213 | ```
214 |
215 | If you use golang JSON structs as responses from your API, you may want to have a common prefix for all the generated models:
216 |
217 | ```golang
218 | converter := typescriptify.New().
219 | converter.Prefix = "API_"
220 | converter.Add(Person{})
221 | ```
222 |
223 | The model name will be `API_Person` instead of `Person`.
224 |
225 | ## Field comments
226 |
227 | Field documentation comments can be added with the `ts_doc` tag:
228 |
229 | ```golang
230 | type Person struct {
231 | Name string `json:"name" ts_doc:"This is a comment"`
232 | }
233 | ```
234 |
235 | Generated typescript:
236 |
237 | ```typescript
238 | export class Person {
239 | /** This is a comment */
240 | name: string;
241 | }
242 | ```
243 |
244 | ## Custom types
245 |
246 | If your field has a type not supported by typescriptify which can be JSONized as is, then you can use the `ts_type` tag to specify the typescript type to use:
247 |
248 | ```golang
249 | type Data struct {
250 | Counters map[string]int `json:"counters" ts_type:"CustomType"`
251 | }
252 | ```
253 |
254 | ...will create:
255 |
256 | ```typescript
257 | export class Data {
258 | counters: CustomType;
259 | }
260 | ```
261 |
262 | If the JSON field needs some special handling before converting it to a javascript object, use `ts_transform`.
263 | For example:
264 |
265 | ```golang
266 | type Data struct {
267 | Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"`
268 | }
269 | ```
270 |
271 | Generated typescript:
272 |
273 | ```typescript
274 | export class Date {
275 | time: Date;
276 |
277 | constructor(source: any = {}) {
278 | if ('string' === typeof source) source = JSON.parse(source);
279 | this.time = new Date(source["time"]);
280 | }
281 | }
282 | ```
283 |
284 | In this case, you should always use `new Data(json)` instead of just casting `json`.
285 |
286 | If you use a custom type that has to be imported, you can do the following:
287 |
288 | ```golang
289 | converter := typescriptify.New()
290 | converter.AddImport("import Decimal from 'decimal.js'")
291 | ```
292 |
293 | This will put your import on top of the generated file.
294 |
295 | ## Global custom types
296 |
297 | Additionally, you can tell the library to automatically use a given Typescript type and custom transformation for a type:
298 |
299 | ```golang
300 | converter := New()
301 | converter.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"})
302 | ```
303 |
304 | If you only want to change `ts_transform` but not `ts_type`, you can pass an empty string.
305 |
306 | ## Enums
307 |
308 | There are two ways to create enums.
309 |
310 | ### Enums with TSName()
311 |
312 | In this case you must provide a list of enum values and the enum type must have a `TSName() string` method
313 |
314 | ```golang
315 | type Weekday int
316 |
317 | const (
318 | Sunday Weekday = iota
319 | Monday
320 | Tuesday
321 | Wednesday
322 | Thursday
323 | Friday
324 | Saturday
325 | )
326 |
327 | var AllWeekdays = []Weekday{ Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, }
328 |
329 | func (w Weekday) TSName() string {
330 | switch w {
331 | case Sunday:
332 | return "SUNDAY"
333 | case Monday:
334 | return "MONDAY"
335 | case Tuesday:
336 | return "TUESDAY"
337 | case Wednesday:
338 | return "WEDNESDAY"
339 | case Thursday:
340 | return "THURSDAY"
341 | case Friday:
342 | return "FRIDAY"
343 | case Saturday:
344 | return "SATURDAY"
345 | default:
346 | return "???"
347 | }
348 | }
349 | ```
350 |
351 | If this is too verbose for you, you can also provide a list of enums and enum names:
352 |
353 | ```golang
354 | var AllWeekdays = []struct {
355 | Value Weekday
356 | TSName string
357 | }{
358 | {Sunday, "SUNDAY"},
359 | {Monday, "MONDAY"},
360 | {Tuesday, "TUESDAY"},
361 | {Wednesday, "WEDNESDAY"},
362 | {Thursday, "THURSDAY"},
363 | {Friday, "FRIDAY"},
364 | {Saturday, "SATURDAY"},
365 | }
366 | ```
367 |
368 | Then, when converting models `AddEnum()` to specify the enum:
369 |
370 | ```golang
371 | converter := New().
372 | AddEnum(AllWeekdays)
373 | ```
374 |
375 | The resulting code will be:
376 |
377 | ```typescript
378 | export enum Weekday {
379 | SUNDAY = 0,
380 | MONDAY = 1,
381 | TUESDAY = 2,
382 | WEDNESDAY = 3,
383 | THURSDAY = 4,
384 | FRIDAY = 5,
385 | SATURDAY = 6,
386 | }
387 | export class Holliday {
388 | name: string;
389 | weekday: Weekday;
390 | }
391 | ```
392 |
393 | ## License
394 |
395 | This library is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
396 |
397 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [2015-] [Tomo Krajina]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/typescriptify/typescriptify.go:
--------------------------------------------------------------------------------
1 | package typescriptify
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "path"
8 | "reflect"
9 | "strings"
10 | "time"
11 |
12 | "github.com/tkrajina/go-reflector/reflector"
13 | )
14 |
15 | const (
16 | tsDocTag = "ts_doc"
17 | tsTransformTag = "ts_transform"
18 | tsType = "ts_type"
19 | jsonTag = "json"
20 | tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any {
21 | if (!a) {
22 | return a;
23 | }
24 | if (Array.isArray(a)) {
25 | return (a as any[]).map(elem => this.convertValues(elem, classs));
26 | } else if ("object" === typeof a) {
27 | if (asMap) {
28 | for (const key of Object.keys(a)) {
29 | a[key] = new classs(a[key]);
30 | }
31 | return a;
32 | }
33 | return new classs(a);
34 | }
35 | return a;
36 | }`
37 | )
38 |
39 | // TypeOptions overrides options set by `ts_*` tags.
40 | type TypeOptions struct {
41 | TSType string
42 | TSDoc string
43 | TSTransform string
44 | }
45 |
46 | // StructType stores settings for transforming one Golang struct.
47 | type StructType struct {
48 | Type reflect.Type
49 | FieldOptions map[reflect.Type]TypeOptions
50 | }
51 |
52 | func NewStruct(i interface{}) *StructType {
53 | return &StructType{
54 | Type: reflect.TypeOf(i),
55 | }
56 | }
57 |
58 | func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType {
59 | if st.FieldOptions == nil {
60 | st.FieldOptions = map[reflect.Type]TypeOptions{}
61 | }
62 | var typ reflect.Type
63 | if ty, is := i.(reflect.Type); is {
64 | typ = ty
65 | } else {
66 | typ = reflect.TypeOf(i)
67 | }
68 | st.FieldOptions[typ] = opts
69 | return st
70 | }
71 |
72 | type EnumType struct {
73 | Type reflect.Type
74 | }
75 |
76 | type enumElement struct {
77 | value interface{}
78 | name string
79 | }
80 |
81 | type TypeScriptify struct {
82 | Prefix string
83 | Suffix string
84 | Indent string
85 | CreateFromMethod bool
86 | CreateConstructor bool
87 | BackupDir string // If empty no backup
88 | DontExport bool
89 | CreateInterface bool
90 | CustomJsonTag string
91 | customImports []string
92 | customCodeBefore []string
93 | customCodeAfter []string
94 | silent bool
95 |
96 | structTypes []StructType
97 | enumTypes []EnumType
98 | enums map[reflect.Type][]enumElement
99 | kinds map[reflect.Kind]string
100 |
101 | fieldTypeOptions map[reflect.Type]TypeOptions
102 |
103 | // throwaway, used when converting
104 | alreadyConverted map[reflect.Type]bool
105 | }
106 |
107 | func New() *TypeScriptify {
108 | result := new(TypeScriptify)
109 | result.Indent = "\t"
110 | result.BackupDir = "."
111 |
112 | kinds := make(map[reflect.Kind]string)
113 |
114 | kinds[reflect.Bool] = "boolean"
115 | kinds[reflect.Interface] = "any"
116 |
117 | kinds[reflect.Int] = "number"
118 | kinds[reflect.Int8] = "number"
119 | kinds[reflect.Int16] = "number"
120 | kinds[reflect.Int32] = "number"
121 | kinds[reflect.Int64] = "number"
122 | kinds[reflect.Uint] = "number"
123 | kinds[reflect.Uint8] = "number"
124 | kinds[reflect.Uint16] = "number"
125 | kinds[reflect.Uint32] = "number"
126 | kinds[reflect.Uint64] = "number"
127 | kinds[reflect.Float32] = "number"
128 | kinds[reflect.Float64] = "number"
129 |
130 | kinds[reflect.String] = "string"
131 |
132 | result.kinds = kinds
133 |
134 | result.Indent = " "
135 | result.CreateFromMethod = false
136 | result.CreateConstructor = true
137 |
138 | return result
139 | }
140 |
141 | func deepFields(typeOf reflect.Type) []reflect.StructField {
142 | fields := make([]reflect.StructField, 0)
143 |
144 | if typeOf.Kind() == reflect.Ptr {
145 | typeOf = typeOf.Elem()
146 | }
147 |
148 | if typeOf.Kind() != reflect.Struct {
149 | return fields
150 | }
151 |
152 | for i := 0; i < typeOf.NumField(); i++ {
153 | f := typeOf.Field(i)
154 |
155 | kind := f.Type.Kind()
156 | if f.Anonymous && kind == reflect.Struct {
157 | //fmt.Println(v.Interface())
158 | fields = append(fields, deepFields(f.Type)...)
159 | } else if f.Anonymous && kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct {
160 | //fmt.Println(v.Interface())
161 | fields = append(fields, deepFields(f.Type.Elem())...)
162 | } else {
163 | fields = append(fields, f)
164 | }
165 | }
166 |
167 | return fields
168 | }
169 |
170 | func (ts *TypeScriptify) Silent() *TypeScriptify {
171 | ts.silent = true
172 | return ts
173 | }
174 |
175 | func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) {
176 | if ts.silent {
177 | return
178 | }
179 |
180 | fmt.Printf(strings.Repeat(" ", depth)+s+"\n", args...)
181 | }
182 |
183 | // ManageType can define custom options for fields of a specified type.
184 | //
185 | // This can be used instead of setting ts_type and ts_transform for all fields of a certain type.
186 | func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify {
187 | var typ reflect.Type
188 | switch t := fld.(type) {
189 | case reflect.Type:
190 | typ = t
191 | default:
192 | typ = reflect.TypeOf(fld)
193 | }
194 | if t.fieldTypeOptions == nil {
195 | t.fieldTypeOptions = map[reflect.Type]TypeOptions{}
196 | }
197 | t.fieldTypeOptions[typ] = opts
198 | return t
199 | }
200 |
201 | func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify {
202 | t.CreateFromMethod = b
203 | return t
204 | }
205 |
206 | func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify {
207 | t.CreateInterface = b
208 | return t
209 | }
210 |
211 | func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify {
212 | t.CreateConstructor = b
213 | return t
214 | }
215 |
216 | func (t *TypeScriptify) WithIndent(i string) *TypeScriptify {
217 | t.Indent = i
218 | return t
219 | }
220 |
221 | func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify {
222 | t.BackupDir = b
223 | return t
224 | }
225 |
226 | func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify {
227 | t.Prefix = p
228 | return t
229 | }
230 |
231 | func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify {
232 | t.Suffix = s
233 | return t
234 | }
235 |
236 | func (t *TypeScriptify) WithCustomJsonTag(tag string) *TypeScriptify {
237 | t.CustomJsonTag = tag
238 | return t
239 | }
240 |
241 | func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify {
242 | switch ty := obj.(type) {
243 | case StructType:
244 | t.structTypes = append(t.structTypes, ty)
245 | case *StructType:
246 | t.structTypes = append(t.structTypes, *ty)
247 | case reflect.Type:
248 | t.AddType(ty)
249 | default:
250 | t.AddType(reflect.TypeOf(obj))
251 | }
252 | return t
253 | }
254 |
255 | func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify {
256 | t.structTypes = append(t.structTypes, StructType{Type: typeOf})
257 | return t
258 | }
259 |
260 | func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) {
261 | keyType := field.Type.Key()
262 | valueType := field.Type.Elem()
263 | valueTypeName := valueType.Name()
264 | if name, ok := t.types[valueType.Kind()]; ok {
265 | valueTypeName = name
266 | }
267 | if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice {
268 | valueTypeName = valueType.Elem().Name() + "[]"
269 | }
270 | if valueType.Kind() == reflect.Ptr {
271 | valueTypeName = valueType.Elem().Name()
272 | }
273 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
274 |
275 | keyTypeStr := keyType.Name()
276 | // Key should always be string, no need for this:
277 | // _, isSimple := t.types[keyType.Kind()]
278 | // if !isSimple {
279 | // keyTypeStr = t.prefix + keyType.Name() + t.suffix
280 | // }
281 |
282 | if valueType.Kind() == reflect.Struct {
283 | t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, t.prefix+valueTypeName))
284 | t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = this.convertValues(source[\"%s\"], %s, true);", t.indent, t.indent, strippedFieldName, strippedFieldName, t.prefix+valueTypeName+t.suffix))
285 | } else {
286 | t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, valueTypeName))
287 | t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = source[\"%s\"];", t.indent, t.indent, strippedFieldName, strippedFieldName))
288 | }
289 | }
290 |
291 | func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify {
292 | if t.enums == nil {
293 | t.enums = map[reflect.Type][]enumElement{}
294 | }
295 | items := reflect.ValueOf(values)
296 | if items.Kind() != reflect.Slice {
297 | panic(fmt.Sprintf("Values for %T isn't a slice", values))
298 | }
299 |
300 | var elements []enumElement
301 | for i := 0; i < items.Len(); i++ {
302 | item := items.Index(i)
303 |
304 | var el enumElement
305 | if item.Kind() == reflect.Struct {
306 | r := reflector.New(item.Interface())
307 | val, err := r.Field("Value").Get()
308 | if err != nil {
309 | panic(fmt.Sprint("missing Type field in ", item.Type().String()))
310 | }
311 | name, err := r.Field("TSName").Get()
312 | if err != nil {
313 | panic(fmt.Sprint("missing TSName field in ", item.Type().String()))
314 | }
315 | el.value = val
316 | el.name = name.(string)
317 | } else {
318 | el.value = item.Interface()
319 | if tsNamer, is := item.Interface().(TSNamer); is {
320 | el.name = tsNamer.TSName()
321 | } else {
322 | panic(fmt.Sprint(item.Type().String(), " has no TSName method"))
323 | }
324 | }
325 |
326 | elements = append(elements, el)
327 | }
328 | ty := reflect.TypeOf(elements[0].value)
329 | t.enums[ty] = elements
330 | t.enumTypes = append(t.enumTypes, EnumType{Type: ty})
331 |
332 | return t
333 | }
334 |
335 | // AddEnumValues is deprecated, use `AddEnum()`
336 | func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify {
337 | t.AddEnum(values)
338 | return t
339 | }
340 |
341 | func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) {
342 | if t.CreateFromMethod {
343 | fmt.Fprintln(os.Stderr, "FromMethod METHOD IS DEPRECATED AND WILL BE REMOVED!!!!!!")
344 | }
345 |
346 | t.alreadyConverted = make(map[reflect.Type]bool)
347 | depth := 0
348 |
349 | result := ""
350 | if len(t.customImports) > 0 {
351 | // Put the custom imports, i.e.: `import Decimal from 'decimal.js'`
352 | for _, cimport := range t.customImports {
353 | result += cimport + "\n"
354 | }
355 | }
356 |
357 | if len(t.customCodeBefore) > 0 {
358 | result += "\n"
359 | for _, code := range t.customCodeBefore {
360 | result += "\n" + code + "\n"
361 | }
362 | result += "\n"
363 | }
364 |
365 | for _, enumTyp := range t.enumTypes {
366 | elements := t.enums[enumTyp.Type]
367 | typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements)
368 | if err != nil {
369 | return "", err
370 | }
371 | result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n")
372 | }
373 |
374 | for _, strctTyp := range t.structTypes {
375 | typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode)
376 | if err != nil {
377 | return "", err
378 | }
379 | result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n")
380 | }
381 |
382 | if len(t.customCodeAfter) > 0 {
383 | result += "\n"
384 | for _, code := range t.customCodeAfter {
385 | result += "\n" + code + "\n"
386 | }
387 | result += "\n"
388 | }
389 |
390 | return result, nil
391 | }
392 |
393 | func loadCustomCode(fileName string) (map[string]string, error) {
394 | result := make(map[string]string)
395 | f, err := os.Open(fileName)
396 | if err != nil {
397 | if os.IsNotExist(err) {
398 | return result, nil
399 | }
400 | return result, err
401 | }
402 | defer f.Close()
403 |
404 | bytes, err := io.ReadAll(f)
405 | if err != nil {
406 | return result, err
407 | }
408 |
409 | var currentName string
410 | var currentValue string
411 | lines := strings.Split(string(bytes), "\n")
412 | for _, line := range lines {
413 | trimmedLine := strings.TrimSpace(line)
414 | if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") {
415 | currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1)
416 | currentValue = ""
417 | } else if trimmedLine == "//[end]" {
418 | result[currentName] = strings.TrimRight(currentValue, " \t\r\n")
419 | currentName = ""
420 | currentValue = ""
421 | } else if len(currentName) > 0 {
422 | currentValue += line + "\n"
423 | }
424 | }
425 |
426 | return result, nil
427 | }
428 |
429 | func (t TypeScriptify) backup(fileName string) error {
430 | fileIn, err := os.Open(fileName)
431 | if err != nil {
432 | if !os.IsNotExist(err) {
433 | return err
434 | }
435 | // No neet to backup, just return:
436 | return nil
437 | }
438 | defer fileIn.Close()
439 |
440 | bytes, err := io.ReadAll(fileIn)
441 | if err != nil {
442 | return err
443 | }
444 |
445 | _, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99")))
446 | if t.BackupDir != "" {
447 | backupFn = path.Join(t.BackupDir, backupFn)
448 | }
449 |
450 | return os.WriteFile(backupFn, bytes, os.FileMode(0700))
451 | }
452 |
453 | func (t TypeScriptify) ConvertToFile(fileName string) error {
454 | if len(t.BackupDir) > 0 {
455 | err := t.backup(fileName)
456 | if err != nil {
457 | return err
458 | }
459 | }
460 |
461 | customCode, err := loadCustomCode(fileName)
462 | if err != nil {
463 | return err
464 | }
465 |
466 | f, err := os.Create(fileName)
467 | if err != nil {
468 | return err
469 | }
470 | defer f.Close()
471 |
472 | converted, err := t.Convert(customCode)
473 | if err != nil {
474 | return err
475 | }
476 |
477 | if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil {
478 | return err
479 | }
480 | if _, err := f.WriteString(converted); err != nil {
481 | return err
482 | }
483 | if err != nil {
484 | return err
485 | }
486 |
487 | return nil
488 | }
489 |
490 | type TSNamer interface {
491 | TSName() string
492 | }
493 |
494 | func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) {
495 | t.logf(depth, "Converting enum %s", typeOf.String())
496 | if _, found := t.alreadyConverted[typeOf]; found { // Already converted
497 | return "", nil
498 | }
499 | t.alreadyConverted[typeOf] = true
500 |
501 | entityName := t.Prefix + typeOf.Name() + t.Suffix
502 | result := "enum " + entityName + " {\n"
503 |
504 | for _, val := range elements {
505 | result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value)
506 | }
507 |
508 | result += "}"
509 |
510 | if !t.DontExport {
511 | result = "export " + result
512 | }
513 |
514 | return result, nil
515 | }
516 |
517 | func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions {
518 | // By default use options defined by tags:
519 | opts := TypeOptions{
520 | TSTransform: field.Tag.Get(tsTransformTag),
521 | TSType: field.Tag.Get(tsType),
522 | TSDoc: field.Tag.Get(tsDocTag),
523 | }
524 |
525 | overrides := []TypeOptions{}
526 |
527 | // But there is maybe an struct-specific override:
528 | for _, strct := range t.structTypes {
529 | if strct.FieldOptions == nil {
530 | continue
531 | }
532 | if strct.Type == structType {
533 | if fldOpts, found := strct.FieldOptions[field.Type]; found {
534 | overrides = append(overrides, fldOpts)
535 | }
536 | }
537 | }
538 |
539 | if fldOpts, found := t.fieldTypeOptions[field.Type]; found {
540 | overrides = append(overrides, fldOpts)
541 | }
542 |
543 | for _, o := range overrides {
544 | if o.TSTransform != "" {
545 | opts.TSTransform = o.TSTransform
546 | }
547 | if o.TSType != "" {
548 | opts.TSType = o.TSType
549 | }
550 | }
551 |
552 | return opts
553 | }
554 |
555 | func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string {
556 | jsonFieldName := ""
557 | tag := jsonTag
558 | if t.CustomJsonTag != "" {
559 | tag = t.CustomJsonTag
560 | }
561 | jsonTag := field.Tag.Get(tag)
562 | if len(jsonTag) > 0 {
563 | jsonTagParts := strings.Split(jsonTag, ",")
564 | if len(jsonTagParts) > 0 {
565 | jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent)
566 | }
567 | hasOmitEmpty := false
568 | ignored := false
569 | for _, t := range jsonTagParts {
570 | if t == "" {
571 | break
572 | }
573 | if t == "omitempty" {
574 | hasOmitEmpty = true
575 | break
576 | }
577 | if t == "-" {
578 | ignored = true
579 | break
580 | }
581 | }
582 | if !ignored && isPtr || hasOmitEmpty {
583 | jsonFieldName = fmt.Sprintf("%s?", jsonFieldName)
584 | }
585 | } else if /*field.IsExported()*/ field.PkgPath == "" {
586 | jsonFieldName = field.Name
587 | }
588 | return jsonFieldName
589 | }
590 |
591 | func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) {
592 | if _, found := t.alreadyConverted[typeOf]; found { // Already converted
593 | return "", nil
594 | }
595 | t.logf(depth, "Converting type %s", typeOf.String())
596 |
597 | t.alreadyConverted[typeOf] = true
598 |
599 | entityName := t.Prefix + typeOf.Name() + t.Suffix
600 | result := ""
601 | if t.CreateInterface {
602 | result += fmt.Sprintf("interface %s {\n", entityName)
603 | } else {
604 | result += fmt.Sprintf("class %s {\n", entityName)
605 | }
606 | if !t.DontExport {
607 | result = "export " + result
608 | }
609 | builder := typeScriptClassBuilder{
610 | types: t.kinds,
611 | indent: t.Indent,
612 | prefix: t.Prefix,
613 | suffix: t.Suffix,
614 | }
615 |
616 | fields := deepFields(typeOf)
617 | for _, field := range fields {
618 | isPtr := field.Type.Kind() == reflect.Ptr
619 | if isPtr {
620 | field.Type = field.Type.Elem()
621 | }
622 | jsonFieldName := t.getJSONFieldName(field, isPtr)
623 | if len(jsonFieldName) == 0 || jsonFieldName == "-" {
624 | continue
625 | }
626 |
627 | var err error
628 | fldOpts := t.getFieldOptions(typeOf, field)
629 | if fldOpts.TSDoc != "" {
630 | builder.addFieldDefinitionLine("/** " + fldOpts.TSDoc + " */")
631 | }
632 | if fldOpts.TSTransform != "" {
633 | t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
634 | err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
635 | } else if _, isEnum := t.enums[field.Type]; isEnum {
636 | t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name)
637 | builder.AddEnumField(jsonFieldName, field)
638 | } else if fldOpts.TSType != "" { // Struct:
639 | t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
640 | err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
641 | } else if field.Type.Kind() == reflect.Struct { // Struct:
642 | t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String())
643 | typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode)
644 | if err != nil {
645 | return "", err
646 | }
647 | if typeScriptChunk != "" {
648 | result = typeScriptChunk + "\n" + result
649 | }
650 | builder.AddStructField(jsonFieldName, field)
651 | } else if field.Type.Kind() == reflect.Map {
652 | t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name)
653 | // Also convert map key types if needed
654 | var keyTypeToConvert reflect.Type
655 | switch field.Type.Key().Kind() {
656 | case reflect.Struct:
657 | keyTypeToConvert = field.Type.Key()
658 | case reflect.Ptr:
659 | keyTypeToConvert = field.Type.Key().Elem()
660 | }
661 | if keyTypeToConvert != nil {
662 | typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode)
663 | if err != nil {
664 | return "", err
665 | }
666 | if typeScriptChunk != "" {
667 | result = typeScriptChunk + "\n" + result
668 | }
669 | }
670 | // Also convert map value types if needed
671 | var valueTypeToConvert reflect.Type
672 | switch field.Type.Elem().Kind() {
673 | case reflect.Struct:
674 | valueTypeToConvert = field.Type.Elem()
675 | case reflect.Ptr:
676 | valueTypeToConvert = field.Type.Elem().Elem()
677 | }
678 | if valueTypeToConvert != nil {
679 | typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode)
680 | if err != nil {
681 | return "", err
682 | }
683 | if typeScriptChunk != "" {
684 | result = typeScriptChunk + "\n" + result
685 | }
686 | }
687 |
688 | builder.AddMapField(jsonFieldName, field)
689 | } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice:
690 | if field.Type.Elem().Kind() == reflect.Ptr { //extract ptr type
691 | field.Type = field.Type.Elem()
692 | }
693 |
694 | arrayDepth := 1
695 | for field.Type.Elem().Kind() == reflect.Slice { // Slice of slices:
696 | field.Type = field.Type.Elem()
697 | arrayDepth++
698 | }
699 |
700 | if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs:
701 | t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String())
702 | typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode)
703 | if err != nil {
704 | return "", err
705 | }
706 | if typeScriptChunk != "" {
707 | result = typeScriptChunk + "\n" + result
708 | }
709 | builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth)
710 | } else { // Slice of simple fields:
711 | t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name)
712 | err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts)
713 | }
714 | } else { // Simple field:
715 | t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
716 | err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
717 | }
718 | if err != nil {
719 | return "", err
720 | }
721 | }
722 |
723 | if t.CreateFromMethod {
724 | t.CreateConstructor = true
725 | }
726 |
727 | result += strings.Join(builder.fields, "\n") + "\n"
728 | if !t.CreateInterface {
729 | constructorBody := strings.Join(builder.constructorBody, "\n")
730 | needsConvertValue := strings.Contains(constructorBody, "this.convertValues")
731 | if t.CreateFromMethod {
732 | result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent)
733 | result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName)
734 | result += fmt.Sprintf("%s}\n", t.Indent)
735 | }
736 | if t.CreateConstructor {
737 | result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent)
738 | result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n"
739 | result += constructorBody + "\n"
740 | result += fmt.Sprintf("%s}\n", t.Indent)
741 | }
742 | if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) {
743 | result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n"
744 | }
745 | }
746 |
747 | if customCode != nil {
748 | code := customCode[entityName]
749 | if len(code) != 0 {
750 | result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n"
751 | }
752 | }
753 |
754 | result += "}"
755 |
756 | return result, nil
757 | }
758 |
759 | func (t *TypeScriptify) AddImport(i string) {
760 | for _, cimport := range t.customImports {
761 | if cimport == i {
762 | return
763 | }
764 | }
765 |
766 | t.customImports = append(t.customImports, i)
767 | }
768 |
769 | func (t *TypeScriptify) WithCustomCodeBefore(i string) {
770 | t.customCodeBefore = append(t.customCodeBefore, i)
771 | }
772 |
773 | func (t *TypeScriptify) WithCustomCodeAfter(i string) {
774 | t.customCodeAfter = append(t.customCodeAfter, i)
775 | }
776 |
777 | type typeScriptClassBuilder struct {
778 | types map[reflect.Kind]string
779 | indent string
780 | fields []string
781 | createFromMethodBody []string
782 | constructorBody []string
783 | prefix, suffix string
784 | }
785 |
786 | func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error {
787 | fieldType, kind := field.Type.Elem().Name(), field.Type.Elem().Kind()
788 | typeScriptType := t.types[kind]
789 |
790 | if len(fieldName) > 0 {
791 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
792 | if len(opts.TSType) > 0 {
793 | t.addField(fieldName, opts.TSType)
794 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
795 | return nil
796 | } else if len(typeScriptType) > 0 {
797 | t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth)))
798 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
799 | return nil
800 | }
801 | }
802 |
803 | return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType)
804 | }
805 |
806 | func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error {
807 | fieldType, kind := field.Type.Name(), field.Type.Kind()
808 |
809 | typeScriptType := t.types[kind]
810 | if len(opts.TSType) > 0 {
811 | typeScriptType = opts.TSType
812 | }
813 |
814 | if len(typeScriptType) > 0 && len(fieldName) > 0 {
815 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
816 | t.addField(fieldName, typeScriptType)
817 | if opts.TSTransform == "" {
818 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
819 | } else {
820 | val := fmt.Sprintf(`source["%s"]`, strippedFieldName)
821 | expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1)
822 | t.addInitializerFieldLine(strippedFieldName, expression)
823 | }
824 | return nil
825 | }
826 |
827 | return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType)
828 | }
829 |
830 | func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) {
831 | fieldType := field.Type.Name()
832 | t.addField(fieldName, t.prefix+fieldType+t.suffix)
833 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
834 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
835 | }
836 |
837 | func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField) {
838 | fieldType := field.Type.Name()
839 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
840 | t.addField(fieldName, t.prefix+fieldType+t.suffix)
841 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix))
842 | }
843 |
844 | func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) {
845 | fieldType := field.Type.Elem().Name()
846 | strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
847 | t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth)))
848 | t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix))
849 | }
850 |
851 | func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) {
852 | t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result.", fld, " = ", initializer, ";"))
853 | t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this.", fld, " = ", initializer, ";"))
854 | }
855 |
856 | func (t *typeScriptClassBuilder) addFieldDefinitionLine(line string) {
857 | t.fields = append(t.fields, t.indent+line)
858 | }
859 |
860 | func (t *typeScriptClassBuilder) addField(fld, fldType string) {
861 | t.fields = append(t.fields, fmt.Sprint(t.indent, fld, ": ", fldType, ";"))
862 | }
863 |
--------------------------------------------------------------------------------
/typescriptify/typescriptify_test.go:
--------------------------------------------------------------------------------
1 | package typescriptify
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 | "reflect"
9 | "strings"
10 | "testing"
11 | "time"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | type Address struct {
17 | // Used in html
18 | Duration float64 `json:"duration" custom:"durationCustom"`
19 | Text1 string `json:"text,omitempty" custom:"textCustom,omitempty"`
20 | // Ignored:
21 | Text2 string `json:",omitempty" custom:",omitempty"`
22 | Text3 string `json:"-" custom:"-"`
23 | }
24 |
25 | type Dummy struct {
26 | Something string `json:"something"`
27 | }
28 |
29 | type HasName struct {
30 | Name string `json:"name"`
31 | }
32 |
33 | type Person struct {
34 | HasName
35 | Nicknames []string `json:"nicknames"`
36 | Addresses []Address `json:"addresses"`
37 | Address *Address `json:"address"`
38 | Metadata string `json:"metadata" ts_type:"{[key:string]:string}" ts_transform:"JSON.parse(__VALUE__ || \"{}\")"`
39 | Friends []*Person `json:"friends"`
40 | Dummy Dummy `json:"a"`
41 | }
42 |
43 | func TestTypescriptifyWithTypes(t *testing.T) {
44 | t.Parallel()
45 | converter := New()
46 |
47 | converter.AddType(reflect.TypeOf(Person{}))
48 | converter.CreateConstructor = false
49 | converter.BackupDir = ""
50 |
51 | desiredResult := `export class Dummy {
52 | something: string;
53 | }
54 | export class Address {
55 | duration: number;
56 | text?: string;
57 | }
58 | export class Person {
59 | name: string;
60 | nicknames: string[];
61 | addresses: Address[];
62 | address?: Address;
63 | metadata: {[key:string]:string};
64 | friends: Person[];
65 | a: Dummy;
66 | }`
67 | testConverter(t, converter, false, desiredResult, nil)
68 | }
69 |
70 | func TestTypescriptifyWithCustomCode(t *testing.T) {
71 | t.Parallel()
72 | converter := New()
73 |
74 | converter.WithCustomCodeBefore(`"a"
75 | "b"
76 | "b"`)
77 | converter.WithCustomCodeAfter(`"d"
78 | "e"
79 | "f"`)
80 | converter.AddType(reflect.TypeOf(Person{}))
81 | converter.CreateConstructor = false
82 | converter.BackupDir = ""
83 |
84 | desiredResult := `"a"
85 | "b"
86 | "b"
87 |
88 |
89 | export class Dummy {
90 | something: string;
91 | }
92 | export class Address {
93 | duration: number;
94 | text?: string;
95 | }
96 | export class Person {
97 | name: string;
98 | nicknames: string[];
99 | addresses: Address[];
100 | address?: Address;
101 | metadata: {[key:string]:string};
102 | friends: Person[];
103 | a: Dummy;
104 | }
105 |
106 | "d"
107 | "e"
108 | "f"`
109 | testConverter(t, converter, false, desiredResult, nil)
110 | }
111 |
112 | func TestTypescriptifyWithCustomImports(t *testing.T) {
113 | t.Parallel()
114 | converter := New()
115 |
116 | converter.AddType(reflect.TypeOf(Person{}))
117 | converter.BackupDir = ""
118 | converter.AddImport("//import { Decimal } from 'decimal.js'")
119 | converter.CreateConstructor = false
120 |
121 | desiredResult := `
122 | //import { Decimal } from 'decimal.js'
123 |
124 | export class Dummy {
125 | something: string;
126 | }
127 | export class Address {
128 | duration: number;
129 | text?: string;
130 | }
131 | export class Person {
132 | name: string;
133 | nicknames: string[];
134 | addresses: Address[];
135 | address?: Address;
136 | metadata: {[key:string]:string};
137 | friends: Person[];
138 | a: Dummy;
139 | }`
140 | testConverter(t, converter, false, desiredResult, nil)
141 | }
142 |
143 | func TestTypescriptifyWithInstances(t *testing.T) {
144 | t.Parallel()
145 | converter := New()
146 |
147 | converter.Add(Person{})
148 | converter.Add(Dummy{})
149 | converter.DontExport = true
150 | converter.BackupDir = ""
151 | converter.CreateConstructor = false
152 |
153 | desiredResult := `class Dummy {
154 | something: string;
155 | }
156 | class Address {
157 | duration: number;
158 | text?: string;
159 | }
160 | class Person {
161 | name: string;
162 | nicknames: string[];
163 | addresses: Address[];
164 | address?: Address;
165 | metadata: {[key:string]:string};
166 | friends: Person[];
167 | a: Dummy;
168 | }`
169 | testConverter(t, converter, false, desiredResult, nil)
170 | }
171 |
172 | func TestTypescriptifyWithInterfaces(t *testing.T) {
173 | t.Parallel()
174 | converter := New()
175 |
176 | converter.Add(Person{})
177 | converter.Add(Dummy{})
178 | converter.DontExport = true
179 | converter.BackupDir = ""
180 | converter.CreateInterface = true
181 |
182 | desiredResult := `interface Dummy {
183 | something: string;
184 | }
185 | interface Address {
186 | duration: number;
187 | text?: string;
188 | }
189 | interface Person {
190 | name: string;
191 | nicknames: string[];
192 | addresses: Address[];
193 | address?: Address;
194 | metadata: {[key:string]:string};
195 | friends: Person[];
196 | a: Dummy;
197 | }`
198 | testConverter(t, converter, true, desiredResult, nil)
199 | }
200 |
201 | func TestTypescriptifyWithDoubleClasses(t *testing.T) {
202 | t.Parallel()
203 | converter := New()
204 |
205 | converter.AddType(reflect.TypeOf(Person{}))
206 | converter.AddType(reflect.TypeOf(Person{}))
207 | converter.CreateConstructor = false
208 | converter.BackupDir = ""
209 |
210 | desiredResult := `export class Dummy {
211 | something: string;
212 | }
213 | export class Address {
214 | duration: number;
215 | text?: string;
216 | }
217 | export class Person {
218 | name: string;
219 | nicknames: string[];
220 | addresses: Address[];
221 | address?: Address;
222 | metadata: {[key:string]:string};
223 | friends: Person[];
224 | a: Dummy;
225 | }`
226 | testConverter(t, converter, false, desiredResult, nil)
227 | }
228 |
229 | func TestWithPrefixes(t *testing.T) {
230 | t.Parallel()
231 | converter := New()
232 |
233 | converter.Prefix = "test_"
234 | converter.Suffix = "_test"
235 |
236 | converter.Add(Person{})
237 | converter.DontExport = true
238 | converter.BackupDir = ""
239 |
240 | desiredResult := `class test_Dummy_test {
241 | something: string;
242 |
243 | constructor(source: any = {}) {
244 | if ('string' === typeof source) source = JSON.parse(source);
245 | this.something = source["something"];
246 | }
247 | }
248 | class test_Address_test {
249 | duration: number;
250 | text?: string;
251 |
252 | constructor(source: any = {}) {
253 | if ('string' === typeof source) source = JSON.parse(source);
254 | this.duration = source["duration"];
255 | this.text = source["text"];
256 | }
257 | }
258 | class test_Person_test {
259 | name: string;
260 | nicknames: string[];
261 | addresses: test_Address_test[];
262 | address?: test_Address_test;
263 | metadata: {[key:string]:string};
264 | friends: test_Person_test[];
265 | a: test_Dummy_test;
266 |
267 | constructor(source: any = {}) {
268 | if ('string' === typeof source) source = JSON.parse(source);
269 | this.name = source["name"];
270 | this.nicknames = source["nicknames"];
271 | this.addresses = this.convertValues(source["addresses"], test_Address_test);
272 | this.address = this.convertValues(source["address"], test_Address_test);
273 | this.metadata = JSON.parse(source["metadata"] || "{}");
274 | this.friends = this.convertValues(source["friends"], test_Person_test);
275 | this.a = this.convertValues(source["a"], test_Dummy_test);
276 | }
277 |
278 | ` + tsConvertValuesFunc + `
279 | }`
280 | jsn := jsonizeOrPanic(Person{
281 | Address: &Address{Text1: "txt1"},
282 | Addresses: []Address{{Text1: "111"}},
283 | Metadata: `{"something": "aaa"}`,
284 | })
285 | testConverter(t, converter, true, desiredResult, []string{
286 | `new test_Person_test()`,
287 | `JSON.stringify(new test_Person_test()?.metadata) === "{}"`,
288 | `!(new test_Person_test()?.address)`,
289 | `!(new test_Person_test()?.addresses)`,
290 | `!(new test_Person_test()?.addresses)`,
291 |
292 | `new test_Person_test(` + jsn + ` as any)`,
293 | `new test_Person_test(` + jsn + ` as any)?.metadata?.something === "aaa"`,
294 | `(new test_Person_test(` + jsn + ` as any)?.address as test_Address_test).text === "txt1"`,
295 | `new test_Person_test(` + jsn + ` as any)?.addresses?.length === 1`,
296 | `(new test_Person_test(` + jsn + ` as any)?.addresses[0] as test_Address_test)?.text === "111"`,
297 | })
298 | }
299 |
300 | func testConverter(t *testing.T, converter *TypeScriptify, strictMode bool, desiredResult string, tsExpressionAndDesiredResults []string) {
301 | typeScriptCode, err := converter.Convert(nil)
302 | if err != nil {
303 | panic(err.Error())
304 | }
305 |
306 | fmt.Println("----------------------------------------------------------------------------------------------------")
307 | fmt.Println(desiredResult)
308 | fmt.Println("----------------------------------------------------------------------------------------------------")
309 | fmt.Println(typeScriptCode)
310 | fmt.Println("----------------------------------------------------------------------------------------------------")
311 |
312 | desiredResult = strings.TrimSpace(desiredResult)
313 | typeScriptCode = strings.Trim(typeScriptCode, " \t\n\r")
314 | if typeScriptCode != desiredResult {
315 | gotLines1 := strings.Split(typeScriptCode, "\n")
316 | expectedLines2 := strings.Split(desiredResult, "\n")
317 |
318 | max := len(gotLines1)
319 | if len(expectedLines2) > max {
320 | max = len(expectedLines2)
321 | }
322 |
323 | for i := 0; i < max; i++ {
324 | var gotLine, expectedLine string
325 | if i < len(gotLines1) {
326 | gotLine = gotLines1[i]
327 | }
328 | if i < len(expectedLines2) {
329 | expectedLine = expectedLines2[i]
330 | }
331 | if assert.Equal(t, strings.TrimSpace(expectedLine), strings.TrimSpace(gotLine), "line #%d", 1+i) {
332 | fmt.Printf("OK: %s\n", gotLine)
333 | } else {
334 | t.FailNow()
335 | }
336 | }
337 | }
338 |
339 | if t.Failed() {
340 | t.FailNow()
341 | }
342 |
343 | testTypescriptExpression(t, strictMode, typeScriptCode, tsExpressionAndDesiredResults)
344 | }
345 |
346 | func testTypescriptExpression(t *testing.T, strictMode bool, baseScript string, tsExpressionAndDesiredResults []string) {
347 | f, err := os.CreateTemp(os.TempDir(), "*.ts")
348 | assert.Nil(t, err)
349 | assert.NotNil(t, f)
350 |
351 | if t.Failed() {
352 | t.FailNow()
353 | }
354 |
355 | _, _ = f.WriteString(baseScript)
356 | _, _ = f.WriteString("\n")
357 | for n, expr := range tsExpressionAndDesiredResults {
358 | _, _ = f.WriteString("// " + expr + "\n")
359 | _, _ = f.WriteString(`if (` + expr + `) { console.log("#` + fmt.Sprint(1+n) + ` OK") } else { throw new Error() }`)
360 | _, _ = f.WriteString("\n\n")
361 | }
362 |
363 | fmt.Println("tmp ts: ", f.Name())
364 | var byts []byte
365 | if strictMode {
366 | byts, err = exec.Command("tsc", "--strict", f.Name()).CombinedOutput()
367 | } else {
368 | byts, err = exec.Command("tsc", f.Name()).CombinedOutput()
369 | }
370 | assert.Nil(t, err, string(byts))
371 |
372 | jsFile := strings.Replace(f.Name(), ".ts", ".js", 1)
373 | fmt.Println("executing:", jsFile)
374 | byts, err = exec.Command("node", jsFile).CombinedOutput()
375 | assert.Nil(t, err, string(byts))
376 | }
377 |
378 | func TestTypescriptifyCustomType(t *testing.T) {
379 | t.Parallel()
380 | type TestCustomType struct {
381 | Map map[string]int `json:"map" ts_type:"{[key: string]: number}"`
382 | }
383 |
384 | converter := New()
385 |
386 | converter.AddType(reflect.TypeOf(TestCustomType{}))
387 | converter.BackupDir = ""
388 | converter.CreateConstructor = false
389 |
390 | desiredResult := `export class TestCustomType {
391 | map: {[key: string]: number};
392 | }`
393 | testConverter(t, converter, false, desiredResult, nil)
394 | }
395 |
396 | func TestDate(t *testing.T) {
397 | t.Parallel()
398 | type TestCustomType struct {
399 | Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"`
400 | }
401 |
402 | converter := New()
403 | converter.AddType(reflect.TypeOf(TestCustomType{}))
404 | converter.BackupDir = ""
405 |
406 | desiredResult := `export class TestCustomType {
407 | time: Date;
408 |
409 | constructor(source: any = {}) {
410 | if ('string' === typeof source) source = JSON.parse(source);
411 | this.time = new Date(source["time"]);
412 | }
413 | }`
414 |
415 | jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)})
416 | testConverter(t, converter, true, desiredResult, []string{
417 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`,
418 | //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`,
419 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`,
420 | })
421 | }
422 |
423 | func TestDateWithoutTags(t *testing.T) {
424 | t.Parallel()
425 | type TestCustomType struct {
426 | Time time.Time `json:"time"`
427 | }
428 |
429 | // Test with custom field options defined per-one-struct:
430 | converter1 := New()
431 | converter1.Add(NewStruct(TestCustomType{}).WithFieldOpts(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"}))
432 | converter1.BackupDir = ""
433 |
434 | // Test with custom field options defined globally:
435 | converter2 := New()
436 | converter2.Add(reflect.TypeOf(TestCustomType{}))
437 | converter2.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"})
438 | converter2.BackupDir = ""
439 |
440 | for _, converter := range []*TypeScriptify{converter1, converter2} {
441 | desiredResult := `export class TestCustomType {
442 | time: Date;
443 |
444 | constructor(source: any = {}) {
445 | if ('string' === typeof source) source = JSON.parse(source);
446 | this.time = new Date(source["time"]);
447 | }
448 | }`
449 |
450 | jsn := jsonizeOrPanic(TestCustomType{Time: time.Date(2020, 10, 9, 8, 9, 0, 0, time.UTC)})
451 | testConverter(t, converter, true, desiredResult, []string{
452 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time instanceof Date`,
453 | //`console.log(new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON())`,
454 | `new TestCustomType(` + jsonizeOrPanic(jsn) + `).time.toJSON() === "2020-10-09T08:09:00.000Z"`,
455 | })
456 | }
457 | }
458 |
459 | func TestRecursive(t *testing.T) {
460 | t.Parallel()
461 | type Test struct {
462 | Children []Test `json:"children"`
463 | }
464 |
465 | converter := New()
466 |
467 | converter.AddType(reflect.TypeOf(Test{}))
468 | converter.BackupDir = ""
469 |
470 | desiredResult := `export class Test {
471 | children: Test[];
472 |
473 | constructor(source: any = {}) {
474 | if ('string' === typeof source) source = JSON.parse(source);
475 | this.children = this.convertValues(source["children"], Test);
476 | }
477 |
478 | ` + tsConvertValuesFunc + `
479 | }`
480 | testConverter(t, converter, true, desiredResult, nil)
481 | }
482 |
483 | func TestArrayOfArrays(t *testing.T) {
484 | t.Parallel()
485 | type Key struct {
486 | Key string `json:"key"`
487 | }
488 | type Keyboard struct {
489 | Keys [][]Key `json:"keys"`
490 | }
491 |
492 | converter := New()
493 |
494 | converter.AddType(reflect.TypeOf(Keyboard{}))
495 | converter.BackupDir = ""
496 |
497 | desiredResult := `export class Key {
498 | key: string;
499 |
500 | constructor(source: any = {}) {
501 | if ('string' === typeof source) source = JSON.parse(source);
502 | this.key = source["key"];
503 | }
504 | }
505 | export class Keyboard {
506 | keys: Key[][];
507 |
508 | constructor(source: any = {}) {
509 | if ('string' === typeof source) source = JSON.parse(source);
510 | this.keys = this.convertValues(source["keys"], Key);
511 | }
512 |
513 | ` + tsConvertValuesFunc + `
514 | }`
515 | testConverter(t, converter, true, desiredResult, nil)
516 | }
517 |
518 | func TestFixedArray(t *testing.T) {
519 | t.Parallel()
520 | type Sub struct{}
521 | type Tmp struct {
522 | Arr [3]string `json:"arr"`
523 | Arr2 [3]Sub `json:"arr2"`
524 | }
525 |
526 | converter := New()
527 |
528 | converter.AddType(reflect.TypeOf(Tmp{}))
529 | converter.BackupDir = ""
530 |
531 | desiredResult := `export class Sub {
532 |
533 |
534 | constructor(source: any = {}) {
535 | if ('string' === typeof source) source = JSON.parse(source);
536 |
537 | }
538 | }
539 | export class Tmp {
540 | arr: string[];
541 | arr2: Sub[];
542 |
543 | constructor(source: any = {}) {
544 | if ('string' === typeof source) source = JSON.parse(source);
545 | this.arr = source["arr"];
546 | this.arr2 = this.convertValues(source["arr2"], Sub);
547 | }
548 |
549 | ` + tsConvertValuesFunc + `
550 | }
551 | `
552 | testConverter(t, converter, true, desiredResult, nil)
553 | }
554 |
555 | func TestAny(t *testing.T) {
556 | t.Parallel()
557 | type Test struct {
558 | Any interface{} `json:"field"`
559 | }
560 |
561 | converter := New()
562 |
563 | converter.AddType(reflect.TypeOf(Test{}))
564 | converter.BackupDir = ""
565 |
566 | desiredResult := `export class Test {
567 | field: any;
568 |
569 | constructor(source: any = {}) {
570 | if ('string' === typeof source) source = JSON.parse(source);
571 | this.field = source["field"];
572 | }
573 | }`
574 | testConverter(t, converter, true, desiredResult, nil)
575 | }
576 |
577 | type NumberTime time.Time
578 |
579 | func (t NumberTime) MarshalJSON() ([]byte, error) {
580 | return []byte(fmt.Sprintf("%d", time.Time(t).Unix())), nil
581 | }
582 |
583 | func TestTypeAlias(t *testing.T) {
584 | t.Parallel()
585 | type Person struct {
586 | Birth NumberTime `json:"birth" ts_type:"number"`
587 | }
588 |
589 | converter := New()
590 |
591 | converter.AddType(reflect.TypeOf(Person{}))
592 | converter.BackupDir = ""
593 | converter.CreateConstructor = false
594 |
595 | desiredResult := `export class Person {
596 | birth: number;
597 | }`
598 | testConverter(t, converter, false, desiredResult, nil)
599 | }
600 |
601 | type MSTime struct {
602 | time.Time
603 | }
604 |
605 | func (MSTime) UnmarshalJSON([]byte) error { return nil }
606 | func (MSTime) MarshalJSON() ([]byte, error) { return []byte("1111"), nil }
607 |
608 | func TestOverrideCustomType(t *testing.T) {
609 | t.Parallel()
610 |
611 | type SomeStruct struct {
612 | Time MSTime `json:"time" ts_type:"number"`
613 | }
614 | var _ json.Marshaler = new(MSTime)
615 | var _ json.Unmarshaler = new(MSTime)
616 |
617 | converter := New()
618 |
619 | converter.AddType(reflect.TypeOf(SomeStruct{}))
620 | converter.BackupDir = ""
621 | converter.CreateConstructor = false
622 |
623 | desiredResult := `export class SomeStruct {
624 | time: number;
625 | }`
626 | testConverter(t, converter, false, desiredResult, nil)
627 |
628 | byts, _ := json.Marshal(SomeStruct{Time: MSTime{Time: time.Now()}})
629 | assert.Equal(t, `{"time":1111}`, string(byts))
630 | }
631 |
632 | type Weekday int
633 |
634 | const (
635 | Sunday Weekday = iota
636 | Monday
637 | Tuesday
638 | Wednesday
639 | Thursday
640 | Friday
641 | Saturday
642 | )
643 |
644 | func (w Weekday) TSName() string {
645 | switch w {
646 | case Sunday:
647 | return "SUNDAY"
648 | case Monday:
649 | return "MONDAY"
650 | case Tuesday:
651 | return "TUESDAY"
652 | case Wednesday:
653 | return "WEDNESDAY"
654 | case Thursday:
655 | return "THURSDAY"
656 | case Friday:
657 | return "FRIDAY"
658 | case Saturday:
659 | return "SATURDAY"
660 | default:
661 | return "???"
662 | }
663 | }
664 |
665 | // One way to specify enums is to list all values and then every one must have a TSName() method
666 | var allWeekdaysV1 = []Weekday{
667 | Sunday,
668 | Monday,
669 | Tuesday,
670 | Wednesday,
671 | Thursday,
672 | Friday,
673 | Saturday,
674 | }
675 |
676 | // Another way to specify enums:
677 | var allWeekdaysV2 = []struct {
678 | Value Weekday
679 | TSName string
680 | }{
681 | {Sunday, "SUNDAY"},
682 | {Monday, "MONDAY"},
683 | {Tuesday, "TUESDAY"},
684 | {Wednesday, "WEDNESDAY"},
685 | {Thursday, "THURSDAY"},
686 | {Friday, "FRIDAY"},
687 | {Saturday, "SATURDAY"},
688 | }
689 |
690 | type Holliday struct {
691 | Name string `json:"name"`
692 | Weekday Weekday `json:"weekday"`
693 | }
694 |
695 | func TestEnum(t *testing.T) {
696 | t.Parallel()
697 | for _, allWeekdays := range []interface{}{allWeekdaysV1, allWeekdaysV2} {
698 | converter := New().
699 | AddType(reflect.TypeOf(Holliday{})).
700 | AddEnum(allWeekdays).
701 | WithConstructor(true).
702 | WithBackupDir("")
703 |
704 | desiredResult := `export enum Weekday {
705 | SUNDAY = 0,
706 | MONDAY = 1,
707 | TUESDAY = 2,
708 | WEDNESDAY = 3,
709 | THURSDAY = 4,
710 | FRIDAY = 5,
711 | SATURDAY = 6,
712 | }
713 | export class Holliday {
714 | name: string;
715 | weekday: Weekday;
716 |
717 | constructor(source: any = {}) {
718 | if ('string' === typeof source) source = JSON.parse(source);
719 | this.name = source["name"];
720 | this.weekday = source["weekday"];
721 | }
722 | }`
723 | testConverter(t, converter, true, desiredResult, nil)
724 | }
725 | }
726 |
727 | type Gender string
728 |
729 | const (
730 | MaleStr Gender = "m"
731 | FemaleStr Gender = "f"
732 | )
733 |
734 | var allGenders = []struct {
735 | Value Gender
736 | TSName string
737 | }{
738 | {MaleStr, "MALE"},
739 | {FemaleStr, "FEMALE"},
740 | }
741 |
742 | func TestEnumWithStringValues(t *testing.T) {
743 | t.Parallel()
744 | converter := New().
745 | AddEnum(allGenders).
746 | WithConstructor(false).
747 | WithBackupDir("")
748 |
749 | desiredResult := `
750 | export enum Gender {
751 | MALE = "m",
752 | FEMALE = "f",
753 | }
754 | `
755 | testConverter(t, converter, true, desiredResult, nil)
756 | }
757 |
758 | func TestConstructorWithReferences(t *testing.T) {
759 | t.Parallel()
760 | converter := New().
761 | AddType(reflect.TypeOf(Person{})).
762 | AddEnum(allWeekdaysV2).
763 | WithConstructor(true).
764 | WithBackupDir("")
765 |
766 | desiredResult := `export enum Weekday {
767 | SUNDAY = 0,
768 | MONDAY = 1,
769 | TUESDAY = 2,
770 | WEDNESDAY = 3,
771 | THURSDAY = 4,
772 | FRIDAY = 5,
773 | SATURDAY = 6,
774 | }
775 | export class Dummy {
776 | something: string;
777 |
778 | constructor(source: any = {}) {
779 | if ('string' === typeof source) source = JSON.parse(source);
780 | this.something = source["something"];
781 | }
782 | }
783 | export class Address {
784 | duration: number;
785 | text?: string;
786 |
787 | constructor(source: any = {}) {
788 | if ('string' === typeof source) source = JSON.parse(source);
789 | this.duration = source["duration"];
790 | this.text = source["text"];
791 | }
792 | }
793 | export class Person {
794 | name: string;
795 | nicknames: string[];
796 | addresses: Address[];
797 | address?: Address;
798 | metadata: {[key:string]:string};
799 | friends: Person[];
800 | a: Dummy;
801 |
802 | constructor(source: any = {}) {
803 | if ('string' === typeof source) source = JSON.parse(source);
804 | this.name = source["name"];
805 | this.nicknames = source["nicknames"];
806 | this.addresses = this.convertValues(source["addresses"], Address);
807 | this.address = this.convertValues(source["address"], Address);
808 | this.metadata = JSON.parse(source["metadata"] || "{}");
809 | this.friends = this.convertValues(source["friends"], Person);
810 | this.a = this.convertValues(source["a"], Dummy);
811 | }
812 |
813 | ` + tsConvertValuesFunc + `
814 | }`
815 | testConverter(t, converter, true, desiredResult, nil)
816 | }
817 |
818 | type WithMap struct {
819 | Map map[string]int `json:"simpleMap"`
820 | MapObjects map[string]Address `json:"mapObjects"`
821 | PtrMap *map[string]Address `json:"ptrMapObjects"`
822 | }
823 |
824 | func TestMaps(t *testing.T) {
825 | t.Parallel()
826 | converter := New().
827 | AddType(reflect.TypeOf(WithMap{})).
828 | WithConstructor(true).
829 | WithPrefix("API_").
830 | WithBackupDir("")
831 |
832 | desiredResult := `
833 | export class API_Address {
834 | duration: number;
835 | text?: string;
836 |
837 | constructor(source: any = {}) {
838 | if ('string' === typeof source) source = JSON.parse(source);
839 | this.duration = source["duration"];
840 | this.text = source["text"];
841 | }
842 | }
843 | export class API_WithMap {
844 | simpleMap: {[key: string]: number};
845 | mapObjects: {[key: string]: API_Address};
846 | ptrMapObjects?: {[key: string]: API_Address};
847 |
848 | constructor(source: any = {}) {
849 | if ('string' === typeof source) source = JSON.parse(source);
850 | this.simpleMap = source["simpleMap"];
851 | this.mapObjects = this.convertValues(source["mapObjects"], API_Address, true);
852 | this.ptrMapObjects = this.convertValues(source["ptrMapObjects"], API_Address, true);
853 | }
854 |
855 | ` + tsConvertValuesFunc + `
856 | }
857 | `
858 |
859 | json := WithMap{
860 | Map: map[string]int{"aaa": 1},
861 | MapObjects: map[string]Address{"bbb": {Duration: 1.0, Text1: "txt1"}},
862 | PtrMap: &map[string]Address{"ccc": {Duration: 2.0, Text1: "txt2"}},
863 | }
864 |
865 | testConverter(t, converter, true, desiredResult, []string{
866 | `new API_WithMap(` + jsonizeOrPanic(json) + `).simpleMap.aaa == 1`,
867 | `(new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_Address`,
868 | `!((new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb) instanceof API_WithMap)`,
869 | `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.duration == 1`,
870 | `new API_WithMap(` + jsonizeOrPanic(json) + `).mapObjects.bbb.text === "txt1"`,
871 | `(new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_Address`,
872 | `!((new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc) instanceof API_WithMap)`,
873 | `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.duration === 2`,
874 | `new API_WithMap(` + jsonizeOrPanic(json) + `)?.ptrMapObjects?.ccc?.text === "txt2"`,
875 | })
876 | }
877 |
878 | func TestPTR(t *testing.T) {
879 | t.Parallel()
880 | type Person struct {
881 | Name *string `json:"name"`
882 | }
883 |
884 | converter := New()
885 | converter.BackupDir = ""
886 | converter.CreateConstructor = false
887 | converter.Add(Person{})
888 |
889 | desiredResult := `export class Person {
890 | name?: string;
891 | }`
892 | testConverter(t, converter, true, desiredResult, nil)
893 | }
894 |
895 | type PersonWithPtrName struct {
896 | *HasName
897 | }
898 |
899 | func TestAnonymousPtr(t *testing.T) {
900 | t.Parallel()
901 | var p PersonWithPtrName
902 | p.HasName = &HasName{}
903 | p.Name = "JKLJKL"
904 | converter := New().
905 | AddType(reflect.TypeOf(PersonWithPtrName{})).
906 | WithConstructor(true).
907 | WithBackupDir("")
908 |
909 | desiredResult := `
910 | export class PersonWithPtrName {
911 | name: string;
912 |
913 | constructor(source: any = {}) {
914 | if ('string' === typeof source) source = JSON.parse(source);
915 | this.name = source["name"];
916 | }
917 | }
918 | `
919 | testConverter(t, converter, true, desiredResult, nil)
920 | }
921 |
922 | func jsonizeOrPanic(i interface{}) string {
923 | byts, err := json.Marshal(i)
924 | if err != nil {
925 | panic(err)
926 | }
927 | return string(byts)
928 | }
929 |
930 | func TestTestConverter(t *testing.T) {
931 | t.Parallel()
932 |
933 | ts := `class Converter {
934 | ` + tsConvertValuesFunc + `
935 | }
936 | const converter = new Converter();
937 |
938 | class Address {
939 | street: string;
940 | number: number;
941 |
942 | constructor(a: any) {
943 | this.street = a["street"];
944 | this.number = a["number"];
945 | }
946 | }
947 | `
948 |
949 | testTypescriptExpression(t, true, ts, []string{
950 | `(converter.convertValues(null, Address)) === null`,
951 | `(converter.convertValues([], Address)).length === 0`,
952 | `(converter.convertValues({}, Address)) instanceof Address`,
953 | `!(converter.convertValues({}, Address, true) instanceof Address)`,
954 |
955 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[]).length == 1`,
956 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0] instanceof Address`,
957 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].number === 19`,
958 | `(converter.convertValues([{street: "aaa", number: 19}] as any, Address) as Address[])[0].street === "aaa"`,
959 |
960 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[]).length == 1`,
961 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0] instanceof Address`,
962 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].number === 19`,
963 | `(converter.convertValues([[{street: "aaa", number: 19}]] as any, Address) as Address[][])[0][0].street === "aaa"`,
964 |
965 | `Object.keys((converter.convertValues({"first": {street: "aaa", number: 19}}, Address, true) as {[_: string]: Address})).length == 1`,
966 | `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"] instanceof Address`,
967 | `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].number === 19`,
968 | `(converter.convertValues({"first": {street: "aaa", number: 19}} as any, Address, true) as {[_: string]: Address})["first"].street === "aaa"`,
969 | })
970 | }
971 |
972 | func TestIgnoredPTR(t *testing.T) {
973 | t.Parallel()
974 | type PersonWithIgnoredPtr struct {
975 | Name string `json:"name"`
976 | Nickname *string `json:"-"`
977 | }
978 |
979 | converter := New()
980 | converter.BackupDir = ""
981 | converter.Add(PersonWithIgnoredPtr{})
982 |
983 | desiredResult := `
984 | export class PersonWithIgnoredPtr {
985 | name: string;
986 |
987 | constructor(source: any = {}) {
988 | if ('string' === typeof source) source = JSON.parse(source);
989 | this.name = source["name"];
990 | }
991 | }
992 | `
993 | testConverter(t, converter, true, desiredResult, nil)
994 | }
995 |
996 | func TestMapWithPrefix(t *testing.T) {
997 | t.Parallel()
998 |
999 | type Example struct {
1000 | Variable map[string]string `json:"variable"`
1001 | }
1002 |
1003 | converter := New().WithPrefix("prefix_").Add(Example{})
1004 |
1005 | desiredResult := `
1006 | export class prefix_Example {
1007 | variable: {[key: string]: string};
1008 |
1009 | constructor(source: any = {}) {
1010 | if ('string' === typeof source) source = JSON.parse(source);
1011 | this.variable = source["variable"];
1012 | }
1013 | }
1014 | `
1015 | testConverter(t, converter, true, desiredResult, nil)
1016 | }
1017 |
1018 | func TestFieldNamesWithoutJSONAnnotation(t *testing.T) {
1019 | t.Parallel()
1020 |
1021 | type WithoutAnnotation struct {
1022 | PublicField string
1023 | privateField string
1024 | }
1025 | var tmp WithoutAnnotation
1026 | tmp.privateField = ""
1027 |
1028 | converter := New().Add(WithoutAnnotation{})
1029 | desiredResult := `
1030 | export class WithoutAnnotation {
1031 | PublicField: string;
1032 |
1033 | constructor(source: any = {}) {
1034 | if ('string' === typeof source) source = JSON.parse(source);
1035 | this.PublicField = source["PublicField"];
1036 | }
1037 | }
1038 | `
1039 | testConverter(t, converter, true, desiredResult, nil)
1040 | }
1041 |
1042 | func TestTypescriptifyComment(t *testing.T) {
1043 | t.Parallel()
1044 | type Person struct {
1045 | Age int `json:"age" ts_doc:"Age comment"`
1046 | Name string `json:"name" ts_doc:"Name comment"`
1047 | }
1048 |
1049 | converter := New()
1050 |
1051 | converter.AddType(reflect.TypeOf(Person{}))
1052 | converter.BackupDir = ""
1053 | converter.CreateConstructor = false
1054 |
1055 | desiredResult := `export class Person {
1056 | /** Age comment */
1057 | age: number;
1058 | /** Name comment */
1059 | name: string;
1060 | }`
1061 | testConverter(t, converter, false, desiredResult, nil)
1062 | }
1063 |
1064 | func TestTypescriptifyCustomJsonTag(t *testing.T) {
1065 | t.Parallel()
1066 |
1067 | converter := New().WithCustomJsonTag("custom")
1068 |
1069 | converter.AddType(reflect.TypeOf(Address{}))
1070 | converter.CreateConstructor = false
1071 | converter.BackupDir = ""
1072 |
1073 | desiredResult := `export class Address {
1074 | durationCustom: number;
1075 | textCustom?: string;
1076 | }`
1077 | testConverter(t, converter, false, desiredResult, nil)
1078 | }
1079 |
--------------------------------------------------------------------------------