diff --git a/v2/cmd/wails/internal/commands/generate/module.go b/v2/cmd/wails/internal/commands/generate/module.go index 91f9309f9..d4b87f392 100644 --- a/v2/cmd/wails/internal/commands/generate/module.go +++ b/v2/cmd/wails/internal/commands/generate/module.go @@ -2,14 +2,15 @@ package generate import ( "fmt" - "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/cmd/wails/internal" - "github.com/wailsapp/wails/v2/internal/shell" "io" "os" "path/filepath" "runtime" "strings" + + "github.com/leaanthony/clir" + "github.com/wailsapp/wails/v2/cmd/wails/internal" + "github.com/wailsapp/wails/v2/internal/shell" ) // AddModuleCommand adds the `module` subcommand for the `generate` command @@ -43,6 +44,8 @@ func AddModuleCommand(app *clir.Cli, parent *clir.Command, w io.Writer) error { } stdout, stderr, err = shell.RunCommand(cwd, filename) + println(stdout) + println(stderr) if err != nil { return fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) } diff --git a/v2/go.mod b/v2/go.mod index 0e28d30db..af1e71085 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -21,7 +21,6 @@ require ( github.com/leaanthony/gosod v1.0.3 github.com/leaanthony/idgen v1.0.0 github.com/leaanthony/slicer v1.5.0 - github.com/leaanthony/typescriptify-golang-structs v0.1.7 github.com/leaanthony/winicon v1.0.0 github.com/matryer/is v1.4.0 github.com/olekukonko/tablewriter v0.0.4 @@ -29,6 +28,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/tc-hib/winres v0.1.5 github.com/tidwall/sjson v1.1.7 + github.com/tkrajina/go-reflector v0.5.5 github.com/wailsapp/mimetype v1.4.1-beta.1.0.20220331112158-6df7e41671fe github.com/wzshiming/ctc v1.2.3 github.com/ztrue/tracerr v0.3.0 @@ -58,7 +58,6 @@ require ( github.com/tidwall/gjson v1.8.0 // indirect github.com/tidwall/match v1.0.3 // indirect github.com/tidwall/pretty v1.1.0 // indirect - github.com/tkrajina/go-reflector v0.5.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae // indirect diff --git a/v2/go.sum b/v2/go.sum index 56f4037ab..f15b9609c 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -82,8 +82,6 @@ github.com/leaanthony/idgen v1.0.0 h1:IZreR+JGEzFV4yeVuBZA25gM0keUoFy+RDUldncQ+J github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA= github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= -github.com/leaanthony/typescriptify-golang-structs v0.1.7 h1:yoznzWzyxkO/iWdlpq+aPcuJ5Y/hpjq/lmgMFmpjwl0= -github.com/leaanthony/typescriptify-golang-structs v0.1.7/go.mod h1:cWtOkiVhMF77e6phAXUcfNwYmMwCJ67Sij24lfvi9Js= github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ= github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k= diff --git a/v2/internal/appng/app_bindings.go b/v2/internal/appng/app_bindings.go index 1db5c266f..5bd82b487 100644 --- a/v2/internal/appng/app_bindings.go +++ b/v2/internal/appng/app_bindings.go @@ -91,28 +91,10 @@ func generateBindings(bindings *binding.Bindings) error { } _ = fs.MkDirs(targetDir) - modelsFile := filepath.Join(targetDir, "models.ts") - err = bindings.WriteTS(modelsFile) - if err != nil { - return err - } - err = bindings.GenerateGoBindings(targetDir) if err != nil { return err } - // Write backend method wrappers - bindingsFilename := filepath.Join(targetDir, "bindings.js") - err = bindings.GenerateBackendJS(bindingsFilename) - if err != nil { - return err - } - - bindingsTypes := filepath.Join(targetDir, "bindings.d.ts") - err = bindings.GenerateBackendTS(bindingsTypes) - if err != nil { - return err - } return nil diff --git a/v2/internal/appng/app_dev.go b/v2/internal/appng/app_dev.go index bd9400265..575be330a 100644 --- a/v2/internal/appng/app_dev.go +++ b/v2/internal/appng/app_dev.go @@ -219,30 +219,12 @@ func generateBindings(bindings *binding.Bindings) error { return err } _ = fs.MkDirs(targetDir) - modelsFile := filepath.Join(targetDir, "models.ts") - err = bindings.WriteTS(modelsFile) - if err != nil { - return err - } err = bindings.GenerateGoBindings(targetDir) if err != nil { return err } - // Write backend method wrappers - bindingsFilename := filepath.Join(targetDir, "bindings.js") - err = bindings.GenerateBackendJS(bindingsFilename) - if err != nil { - return err - } - - bindingsTypes := filepath.Join(targetDir, "bindings.d.ts") - err = bindings.GenerateBackendTS(bindingsTypes) - if err != nil { - return err - } - return nil } diff --git a/v2/internal/binding/assets/package.json b/v2/internal/binding/assets/package.json deleted file mode 100644 index 1b82716c0..000000000 --- a/v2/internal/binding/assets/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "go", - "version": "1.0.0", - "description": "Package to wrap your bound go methods", - "main": "bindings.js", - "types": "bindings.d.ts", - "scripts": {}, - "author": "", - "license": "ISC" -} \ No newline at end of file diff --git a/v2/internal/binding/binding.go b/v2/internal/binding/binding.go index ef33d858f..b1b747c31 100755 --- a/v2/internal/binding/binding.go +++ b/v2/internal/binding/binding.go @@ -1,12 +1,17 @@ package binding import ( + "bufio" + "bytes" "fmt" - "github.com/leaanthony/typescriptify-golang-structs/typescriptify" + "os" + "path/filepath" "reflect" "runtime" "strings" + "github.com/wailsapp/wails/v2/internal/typescriptify" + "github.com/leaanthony/slicer" "github.com/wailsapp/wails/v2/internal/logger" ) @@ -16,24 +21,17 @@ type Bindings struct { logger logger.CustomLogger exemptions slicer.StringSlicer - // Typescript writer - converter *typescriptify.TypeScriptify + structsToGenerateTS map[string]map[string]interface{} } // NewBindings returns a new Bindings object func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}) *Bindings { result := &Bindings{ - db: newDB(), - logger: logger.CustomLogger("Bindings"), - converter: typescriptify.New(), + db: newDB(), + logger: logger.CustomLogger("Bindings"), + structsToGenerateTS: make(map[string]map[string]interface{}), } - // No backups - result.converter.WithBackupDir("") - - // Hack for TS compilation error - result.converter.AddImport("export {};") - for _, exemption := range exemptions { if exemptions == nil { continue @@ -75,10 +73,6 @@ func (b *Bindings) Add(structPtr interface{}) error { return nil } -func (b *Bindings) WriteTS(filename string) error { - return b.converter.ConvertToFile(filename) -} - func (b *Bindings) DB() *DB { return b.db } @@ -86,3 +80,47 @@ func (b *Bindings) DB() *DB { func (b *Bindings) ToJSON() (string, error) { return b.db.ToJSON() } + +func (b *Bindings) WriteModels(modelsDir string) error { + models := map[string]string{} + for packageName, structsToGenerate := range b.structsToGenerateTS { + thisPackageCode := "" + for _, structInterface := range structsToGenerate { + w := typescriptify.New() + w.WithBackupDir("") + w.Add(structInterface) + str, err := w.Convert(nil) + if err != nil { + return err + } + thisPackageCode += str + } + models[packageName] = thisPackageCode + } + + var modelsData bytes.Buffer + for packageName, modelData := range models { + modelsData.WriteString("export namespace " + packageName + " {\n") + sc := bufio.NewScanner(strings.NewReader(modelData)) + for sc.Scan() { + modelsData.WriteString("\t" + sc.Text() + "\n") + } + modelsData.WriteString("\n}\n\n") + } + + filename := filepath.Join(modelsDir, "models.ts") + err := os.WriteFile(filename, modelsData.Bytes(), 0755) + if err != nil { + return err + } + + return nil +} + +func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) { + println("Adding struct:", packageName, structName) + if b.structsToGenerateTS[packageName] == nil { + b.structsToGenerateTS[packageName] = make(map[string]interface{}) + } + b.structsToGenerateTS[packageName][structName] = s +} diff --git a/v2/internal/binding/boundMethod.go b/v2/internal/binding/boundMethod.go index 206ffe252..f6ffdb600 100644 --- a/v2/internal/binding/boundMethod.go +++ b/v2/internal/binding/boundMethod.go @@ -9,12 +9,11 @@ import ( // BoundMethod defines all the data related to a Go method that is // bound to the Wails application type BoundMethod struct { - Name string `json:"name"` - Inputs []*Parameter `json:"inputs,omitempty"` - Outputs []*Parameter `json:"outputs,omitempty"` - Comments string `json:"comments,omitempty"` - Method reflect.Value `json:"-"` - StructNames []string `json:"structNames"` + Name string `json:"name"` + Inputs []*Parameter `json:"inputs,omitempty"` + Outputs []*Parameter `json:"outputs,omitempty"` + Comments string `json:"comments,omitempty"` + Method reflect.Value `json:"-"` } // InputCount returns the number of inputs this bound method has diff --git a/v2/internal/binding/generate.go b/v2/internal/binding/generate.go index 7bbb60b25..b1cc49b79 100644 --- a/v2/internal/binding/generate.go +++ b/v2/internal/binding/generate.go @@ -13,9 +13,6 @@ import ( "github.com/leaanthony/slicer" ) -//go:embed assets/package.json -var packageJSON []byte - func (b *Bindings) GenerateGoBindings(baseDir string) error { store := b.db.store for packageName, structs := range store { @@ -35,7 +32,7 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { tsContent.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT `) - var importClasses slicer.StringSlicer + var importNamespaces slicer.StringSlicer for methodName, methodDetails := range methods { // Generate JS @@ -53,24 +50,23 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { jsoutput.WriteString("\n") // Generate TS - - if len(methodDetails.StructNames) > 0 { - importClasses.AddSlice(methodDetails.StructNames) - } tsBody.WriteString(fmt.Sprintf("\nexport function %s(", methodName)) args.Clear() for count, input := range methodDetails.Inputs { arg := fmt.Sprintf("arg%d", count+1) - args.Add(arg + ":" + goTypeToTypescriptType(input.TypeName, false)) + args.Add(arg + ":" + goTypeToTypescriptType(input.TypeName)) + if strings.ContainsRune(input.TypeName, '.') { + importNamespaces.Add(strings.Split(input.TypeName, ".")[0]) + } } tsBody.WriteString(args.Join(",") + "):") returnType := "Promise" if methodDetails.OutputCount() > 0 { - firstType := goTypeToTypescriptType(methodDetails.Outputs[0].TypeName, false) + firstType := goTypeToTypescriptType(methodDetails.Outputs[0].TypeName) returnType += "<" + firstType if methodDetails.OutputCount() == 2 { - secondType := goTypeToTypescriptType(methodDetails.Outputs[1].TypeName, false) + secondType := goTypeToTypescriptType(methodDetails.Outputs[1].TypeName) returnType += "|" + secondType } returnType += ">" @@ -80,9 +76,9 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { tsBody.WriteString(returnType + ";\n") } - importClasses.Deduplicate() - importClasses.Each(func(class string) { - tsContent.WriteString("import {" + class + "} from '../models';\n") + importNamespaces.Deduplicate() + importNamespaces.Each(func(namespace string) { + tsContent.WriteString("import {" + namespace + "} from '../models';\n") }) tsContent.WriteString(tsBody.String()) @@ -98,202 +94,15 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { } } } + err := b.WriteModels(baseDir) + if err != nil { + println(err) + return err + } return nil } -func (b *Bindings) GenerateBackendJS(targetfile string) error { - - store := b.db.store - var output bytes.Buffer - - output.WriteString(`// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -// ************************************************ -// This file is deprecated and will not be generated -// in the next version of Wails. Bindings are now -// generated in their own files. -// ************************************************ - -`) - - output.WriteString(`const go = {`) - output.WriteString("\n") - - var sortedPackageNames slicer.StringSlicer - for packageName := range store { - sortedPackageNames.Add(packageName) - } - sortedPackageNames.Sort() - sortedPackageNames.Each(func(packageName string) { - packages := store[packageName] - output.WriteString(fmt.Sprintf(" \"%s\": {", packageName)) - output.WriteString("\n") - var sortedStructNames slicer.StringSlicer - for structName := range packages { - sortedStructNames.Add(structName) - } - sortedStructNames.Sort() - - sortedStructNames.Each(func(structName string) { - structs := packages[structName] - output.WriteString(fmt.Sprintf(" \"%s\": {", structName)) - output.WriteString("\n") - - var sortedMethodNames slicer.StringSlicer - for methodName := range structs { - sortedMethodNames.Add(methodName) - } - sortedMethodNames.Sort() - - sortedMethodNames.Each(func(methodName string) { - methodDetails := structs[methodName] - output.WriteString(" /**\n") - output.WriteString(" * " + methodName + "\n") - var args slicer.StringSlicer - for count, input := range methodDetails.Inputs { - arg := fmt.Sprintf("arg%d", count+1) - args.Add(arg) - output.WriteString(fmt.Sprintf(" * @param {%s} %s - Go Type: %s\n", goTypeToJSDocType(input.TypeName, true), arg, input.TypeName)) - } - returnType := "Promise" - returnTypeDetails := "" - if methodDetails.OutputCount() > 0 { - firstType := goTypeToJSDocType(methodDetails.Outputs[0].TypeName, true) - returnType += "<" + firstType - if methodDetails.OutputCount() == 2 { - secondType := goTypeToJSDocType(methodDetails.Outputs[1].TypeName, true) - returnType += "|" + secondType - } - returnType += ">" - returnTypeDetails = " - Go Type: " + methodDetails.Outputs[0].TypeName - } else { - returnType = "Promise" - } - output.WriteString(" * @returns {" + returnType + "} " + returnTypeDetails + "\n") - output.WriteString(" */\n") - argsString := args.Join(", ") - output.WriteString(fmt.Sprintf(" \"%s\": (%s) => {", methodName, argsString)) - output.WriteString("\n") - output.WriteString(fmt.Sprintf(" return window.go.%s.%s.%s(%s);", packageName, structName, methodName, argsString)) - output.WriteString("\n") - output.WriteString(fmt.Sprintf(" },")) - output.WriteString("\n") - - }) - - output.WriteString(" },\n") - }) - - output.WriteString(" },\n\n") - }) - - output.WriteString(`}; -export default go;`) - output.WriteString("\n") - - dir := filepath.Dir(targetfile) - packageJsonFile := filepath.Join(dir, "package.json") - if !fs.FileExists(packageJsonFile) { - err := os.WriteFile(packageJsonFile, packageJSON, 0755) - if err != nil { - return err - } - } - - return os.WriteFile(targetfile, output.Bytes(), 0755) -} - -// GenerateBackendTS generates typescript bindings for -// the bound methods. -func (b *Bindings) GenerateBackendTS(targetfile string) error { - - store := b.db.store - var output bytes.Buffer - - output.WriteString(` - -// ************************************************ -// This file is deprecated and will not be generated -// in the next version of Wails. Bindings are now -// generated in their own files. -// ************************************************ - -`) - - output.WriteString("import * as models from './models';\n\n") - output.WriteString("export interface go {\n") - - var sortedPackageNames slicer.StringSlicer - for packageName := range store { - sortedPackageNames.Add(packageName) - } - sortedPackageNames.Sort() - sortedPackageNames.Each(func(packageName string) { - packages := store[packageName] - output.WriteString(fmt.Sprintf(" \"%s\": {", packageName)) - output.WriteString("\n") - var sortedStructNames slicer.StringSlicer - for structName := range packages { - sortedStructNames.Add(structName) - } - sortedStructNames.Sort() - - sortedStructNames.Each(func(structName string) { - structs := packages[structName] - output.WriteString(fmt.Sprintf(" \"%s\": {", structName)) - output.WriteString("\n") - - var sortedMethodNames slicer.StringSlicer - for methodName := range structs { - sortedMethodNames.Add(methodName) - } - sortedMethodNames.Sort() - - sortedMethodNames.Each(func(methodName string) { - methodDetails := structs[methodName] - output.WriteString(fmt.Sprintf("\t\t%s(", methodName)) - - var args slicer.StringSlicer - for count, input := range methodDetails.Inputs { - arg := fmt.Sprintf("arg%d", count+1) - args.Add(arg + ":" + goTypeToTypescriptType(input.TypeName, true)) - } - output.WriteString(args.Join(",") + "):") - returnType := "Promise" - if methodDetails.OutputCount() > 0 { - firstType := goTypeToTypescriptType(methodDetails.Outputs[0].TypeName, true) - returnType += "<" + firstType - if methodDetails.OutputCount() == 2 { - secondType := goTypeToTypescriptType(methodDetails.Outputs[1].TypeName, true) - returnType += "|" + secondType - } - returnType += ">" - } else { - returnType = "Promise" - } - output.WriteString(returnType + "\n") - }) - - output.WriteString(" },\n") - }) - output.WriteString(" }\n\n") - }) - output.WriteString("}\n") - - globals := ` -declare global { - interface Window { - go: go; - } -} -` - output.WriteString(globals) - return os.WriteFile(targetfile, output.Bytes(), 0755) -} - -func goTypeToJSDocType(input string, useModelsNamespace bool) string { +func goTypeToJSDocType(input string) string { switch true { case input == "interface{}": return "any" @@ -311,23 +120,20 @@ func goTypeToJSDocType(input string, useModelsNamespace bool) string { case input == "[]byte": return "string" case strings.HasPrefix(input, "[]"): - arrayType := goTypeToJSDocType(input[2:], useModelsNamespace) + arrayType := goTypeToJSDocType(input[2:]) return "Array<" + arrayType + ">" default: if strings.ContainsRune(input, '.') { - if useModelsNamespace { - return "models." + strings.Split(input, ".")[1] - } - return strings.Split(input, ".")[1] + return input } return "any" } } -func goTypeToTypescriptType(input string, useModelsNamespace bool) string { +func goTypeToTypescriptType(input string) string { if strings.HasPrefix(input, "[]") { - arrayType := goTypeToJSDocType(input[2:], useModelsNamespace) + arrayType := goTypeToJSDocType(input[2:]) return "Array<" + arrayType + ">" } - return goTypeToJSDocType(input, useModelsNamespace) + return goTypeToJSDocType(input) } diff --git a/v2/internal/binding/reflect.go b/v2/internal/binding/reflect.go index ff0791208..a18e93b44 100755 --- a/v2/internal/binding/reflect.go +++ b/v2/internal/binding/reflect.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "runtime" + "strings" ) // isStructPtr returns true if the value given is a @@ -47,7 +48,9 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { // Process Struct structType := reflect.TypeOf(value) structValue := reflect.ValueOf(value) - baseName := structType.String()[1:] + structTypeString := structType.String() + baseName := structTypeString[1:] + packageName := strings.Split(baseName, ".")[0] // Process Methods for i := 0; i < structType.NumMethod(); i++ { @@ -90,8 +93,8 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { typ := thisInput.Elem() a := reflect.New(typ) s := reflect.Indirect(a).Interface() - b.converter.Add(s) - boundMethod.StructNames = append(boundMethod.StructNames, typ.Name()) + name := typ.Name() + b.AddStructToGenerateTS(packageName, name, s) } } @@ -99,8 +102,8 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { if thisInput.Kind() == reflect.Struct { a := reflect.New(thisInput) s := reflect.Indirect(a).Interface() - b.converter.Add(s) - boundMethod.StructNames = append(boundMethod.StructNames, thisInput.Name()) + name := thisInput.Name() + b.AddStructToGenerateTS(packageName, name, s) } inputs = append(inputs, thisParam) @@ -129,8 +132,8 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { typ := thisOutput.Elem() a := reflect.New(typ) s := reflect.Indirect(a).Interface() - b.converter.Add(s) - boundMethod.StructNames = append(boundMethod.StructNames, typ.Name()) + name := typ.Name() + b.AddStructToGenerateTS(packageName, name, s) } } @@ -138,8 +141,8 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { if thisOutput.Kind() == reflect.Struct { a := reflect.New(thisOutput) s := reflect.Indirect(a).Interface() - b.converter.Add(s) - boundMethod.StructNames = append(boundMethod.StructNames, thisOutput.Name()) + name := thisOutput.Name() + b.AddStructToGenerateTS(packageName, name, s) } outputs = append(outputs, thisParam) diff --git a/v2/internal/typescriptify/LICENSE.txt b/v2/internal/typescriptify/LICENSE.txt new file mode 100644 index 000000000..fa6e64ac4 --- /dev/null +++ b/v2/internal/typescriptify/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2015-] [Tomo Krajina] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/v2/internal/typescriptify/README.md b/v2/internal/typescriptify/README.md new file mode 100644 index 000000000..b5c961835 --- /dev/null +++ b/v2/internal/typescriptify/README.md @@ -0,0 +1,2 @@ +Based on: https://github.com/tkrajina/typescriptify-golang-structs +License: LICENSE.txt \ No newline at end of file diff --git a/v2/internal/typescriptify/typescriptify.go b/v2/internal/typescriptify/typescriptify.go new file mode 100644 index 000000000..8b55093b3 --- /dev/null +++ b/v2/internal/typescriptify/typescriptify.go @@ -0,0 +1,816 @@ +package typescriptify + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "path" + "reflect" + "strings" + "time" + + "github.com/tkrajina/go-reflector/reflector" +) + +const ( + tsTransformTag = "ts_transform" + tsType = "ts_type" + tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; +}` +) + +// TypeOptions overrides options set by `ts_*` tags. +type TypeOptions struct { + TSType string + TSTransform string +} + +// StructType stores settings for transforming one Golang struct. +type StructType struct { + Type reflect.Type + FieldOptions map[reflect.Type]TypeOptions +} + +func NewStruct(i interface{}) *StructType { + return &StructType{ + Type: reflect.TypeOf(i), + } +} + +func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType { + if st.FieldOptions == nil { + st.FieldOptions = map[reflect.Type]TypeOptions{} + } + var typ reflect.Type + if ty, is := i.(reflect.Type); is { + typ = ty + } else { + typ = reflect.TypeOf(i) + } + st.FieldOptions[typ] = opts + return st +} + +type EnumType struct { + Type reflect.Type +} + +type enumElement struct { + value interface{} + name string +} + +type TypeScriptify struct { + Prefix string + Suffix string + Indent string + CreateFromMethod bool + CreateConstructor bool + BackupDir string // If empty no backup + DontExport bool + CreateInterface bool + customImports []string + + structTypes []StructType + enumTypes []EnumType + enums map[reflect.Type][]enumElement + kinds map[reflect.Kind]string + + fieldTypeOptions map[reflect.Type]TypeOptions + + // throwaway, used when converting + alreadyConverted map[reflect.Type]bool +} + +func New() *TypeScriptify { + result := new(TypeScriptify) + result.Indent = "\t" + result.BackupDir = "." + + kinds := make(map[reflect.Kind]string) + + kinds[reflect.Bool] = "boolean" + kinds[reflect.Interface] = "any" + + kinds[reflect.Int] = "number" + kinds[reflect.Int8] = "number" + kinds[reflect.Int16] = "number" + kinds[reflect.Int32] = "number" + kinds[reflect.Int64] = "number" + kinds[reflect.Uint] = "number" + kinds[reflect.Uint8] = "number" + kinds[reflect.Uint16] = "number" + kinds[reflect.Uint32] = "number" + kinds[reflect.Uint64] = "number" + kinds[reflect.Float32] = "number" + kinds[reflect.Float64] = "number" + + kinds[reflect.String] = "string" + + result.kinds = kinds + + result.Indent = " " + result.CreateFromMethod = true + result.CreateConstructor = true + + // if result.CreateFromMethod { + // fmt.Fprintln(os.Stderr, "FromMethod METHOD IS DEPRECATED AND WILL BE REMOVED!!!!!!") + // } + return result +} + +func deepFields(typeOf reflect.Type) []reflect.StructField { + fields := make([]reflect.StructField, 0) + + if typeOf.Kind() == reflect.Ptr { + typeOf = typeOf.Elem() + } + + if typeOf.Kind() != reflect.Struct { + return fields + } + + for i := 0; i < typeOf.NumField(); i++ { + f := typeOf.Field(i) + + kind := f.Type.Kind() + if f.Anonymous && kind == reflect.Struct { + //fmt.Println(v.Interface()) + fields = append(fields, deepFields(f.Type)...) + } else if f.Anonymous && kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct { + //fmt.Println(v.Interface()) + fields = append(fields, deepFields(f.Type.Elem())...) + } else { + fields = append(fields, f) + } + } + + return fields +} + +func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) { + fmt.Printf(strings.Repeat(" ", depth)+s+"\n", args...) +} + +// ManageType can define custom options for fields of a specified type. +// +// This can be used instead of setting ts_type and ts_transform for all fields of a certain type. +func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify { + var typ reflect.Type + switch t := fld.(type) { + case reflect.Type: + typ = t + default: + typ = reflect.TypeOf(fld) + } + if t.fieldTypeOptions == nil { + t.fieldTypeOptions = map[reflect.Type]TypeOptions{} + } + t.fieldTypeOptions[typ] = opts + return t +} + +func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify { + t.CreateFromMethod = b + return t +} + +func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify { + t.CreateInterface = b + return t +} + +func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify { + t.CreateConstructor = b + return t +} + +func (t *TypeScriptify) WithIndent(i string) *TypeScriptify { + t.Indent = i + return t +} + +func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify { + t.BackupDir = b + return t +} + +func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify { + t.Prefix = p + return t +} + +func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify { + t.Suffix = s + return t +} + +func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify { + switch ty := obj.(type) { + case StructType: + t.structTypes = append(t.structTypes, ty) + case *StructType: + t.structTypes = append(t.structTypes, *ty) + case reflect.Type: + t.AddType(ty) + default: + t.AddType(reflect.TypeOf(obj)) + } + return t +} + +func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify { + t.structTypes = append(t.structTypes, StructType{Type: typeOf}) + return t +} + +func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) { + keyType := field.Type.Key() + valueType := field.Type.Elem() + valueTypeName := valueType.Name() + if name, ok := t.types[valueType.Kind()]; ok { + valueTypeName = name + } + if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice { + valueTypeName = valueType.Elem().Name() + "[]" + } + if valueType.Kind() == reflect.Ptr { + valueTypeName = valueType.Elem().Name() + } + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + + keyTypeStr := keyType.Name() + // Key should always be string, no need for this: + // _, isSimple := t.types[keyType.Kind()] + // if !isSimple { + // keyTypeStr = t.prefix + keyType.Name() + t.suffix + // } + + t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, valueTypeName)) + if valueType.Kind() == reflect.Struct { + t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = this.convertValues(source[\"%s\"], %s, true);", t.indent, t.indent, strippedFieldName, strippedFieldName, t.prefix+valueTypeName+t.suffix)) + } else { + t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis.%s = source[\"%s\"];", t.indent, t.indent, strippedFieldName, strippedFieldName)) + } +} + +func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify { + if t.enums == nil { + t.enums = map[reflect.Type][]enumElement{} + } + items := reflect.ValueOf(values) + if items.Kind() != reflect.Slice { + panic(fmt.Sprintf("Values for %T isn't a slice", values)) + } + + var elements []enumElement + for i := 0; i < items.Len(); i++ { + item := items.Index(i) + + var el enumElement + if item.Kind() == reflect.Struct { + r := reflector.New(item.Interface()) + val, err := r.Field("Value").Get() + if err != nil { + panic(fmt.Sprint("missing Type field in ", item.Type().String())) + } + name, err := r.Field("TSName").Get() + if err != nil { + panic(fmt.Sprint("missing TSName field in ", item.Type().String())) + } + el.value = val + el.name = name.(string) + } else { + el.value = item.Interface() + if tsNamer, is := item.Interface().(TSNamer); is { + el.name = tsNamer.TSName() + } else { + panic(fmt.Sprint(item.Type().String(), " has no TSName method")) + } + } + + elements = append(elements, el) + } + ty := reflect.TypeOf(elements[0].value) + t.enums[ty] = elements + t.enumTypes = append(t.enumTypes, EnumType{Type: ty}) + + return t +} + +// AddEnumValues is deprecated, use `AddEnum()` +func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify { + t.AddEnum(values) + return t +} + +func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) { + t.alreadyConverted = make(map[reflect.Type]bool) + depth := 0 + + result := "" + if len(t.customImports) > 0 { + // Put the custom imports, i.e.: `import Decimal from 'decimal.js'` + for _, cimport := range t.customImports { + result += cimport + "\n" + } + } + + for _, enumTyp := range t.enumTypes { + elements := t.enums[enumTyp.Type] + typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements) + if err != nil { + return "", err + } + result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") + } + + for _, strctTyp := range t.structTypes { + typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode) + if err != nil { + return "", err + } + result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") + } + return result, nil +} + +func loadCustomCode(fileName string) (map[string]string, error) { + result := make(map[string]string) + f, err := os.Open(fileName) + if err != nil { + if os.IsNotExist(err) { + return result, nil + } + return result, err + } + defer f.Close() + + bytes, err := ioutil.ReadAll(f) + if err != nil { + return result, err + } + + var currentName string + var currentValue string + lines := strings.Split(string(bytes), "\n") + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") { + currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1) + currentValue = "" + } else if trimmedLine == "//[end]" { + result[currentName] = strings.TrimRight(currentValue, " \t\r\n") + currentName = "" + currentValue = "" + } else if len(currentName) > 0 { + currentValue += line + "\n" + } + } + + return result, nil +} + +func (t TypeScriptify) backup(fileName string) error { + fileIn, err := os.Open(fileName) + if err != nil { + if !os.IsNotExist(err) { + return err + } + // No neet to backup, just return: + return nil + } + defer fileIn.Close() + + bytes, err := ioutil.ReadAll(fileIn) + if err != nil { + return err + } + + _, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99"))) + if t.BackupDir != "" { + backupFn = path.Join(t.BackupDir, backupFn) + } + + return ioutil.WriteFile(backupFn, bytes, os.FileMode(0700)) +} + +func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error { + if len(t.BackupDir) > 0 { + err := t.backup(fileName) + if err != nil { + return err + } + } + + customCode, err := loadCustomCode(fileName) + if err != nil { + return err + } + + f, err := os.Create(fileName) + if err != nil { + return err + } + defer f.Close() + + converted, err := t.Convert(customCode) + if err != nil { + return err + } + + var lines []string + sc := bufio.NewScanner(strings.NewReader(converted)) + for sc.Scan() { + lines = append(lines, "\t"+sc.Text()) + } + + converted = "export namespace " + packageName + " {\n" + converted += strings.Join(lines, "\n") + converted += "\n}\n" + + if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil { + return err + } + if _, err := f.WriteString(converted); err != nil { + return err + } + if err != nil { + return err + } + + return nil +} + +type TSNamer interface { + TSName() string +} + +func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) { + t.logf(depth, "Converting enum %s", typeOf.String()) + if _, found := t.alreadyConverted[typeOf]; found { // Already converted + return "", nil + } + t.alreadyConverted[typeOf] = true + + entityName := t.Prefix + typeOf.Name() + t.Suffix + result := "enum " + entityName + " {\n" + + for _, val := range elements { + result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value) + } + + result += "}" + + if !t.DontExport { + result = "export " + result + } + + return result, nil +} + +func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions { + // By default use options defined by tags: + opts := TypeOptions{TSTransform: field.Tag.Get(tsTransformTag), TSType: field.Tag.Get(tsType)} + + overrides := []TypeOptions{} + + // But there is maybe an struct-specific override: + for _, strct := range t.structTypes { + if strct.FieldOptions == nil { + continue + } + if strct.Type == structType { + if fldOpts, found := strct.FieldOptions[field.Type]; found { + overrides = append(overrides, fldOpts) + } + } + } + + if fldOpts, found := t.fieldTypeOptions[field.Type]; found { + overrides = append(overrides, fldOpts) + } + + for _, o := range overrides { + if o.TSTransform != "" { + opts.TSTransform = o.TSTransform + } + if o.TSType != "" { + opts.TSType = o.TSType + } + } + + return opts +} + +func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string { + jsonFieldName := "" + jsonTag := field.Tag.Get("json") + if len(jsonTag) > 0 { + jsonTagParts := strings.Split(jsonTag, ",") + if len(jsonTagParts) > 0 { + jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent) + } + hasOmitEmpty := false + ignored := false + for _, t := range jsonTagParts { + if t == "" { + break + } + if t == "omitempty" { + hasOmitEmpty = true + break + } + if t == "-" { + ignored = true + break + } + } + if !ignored && isPtr || hasOmitEmpty { + jsonFieldName = fmt.Sprintf("%s?", jsonFieldName) + } + } + return jsonFieldName +} + +func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) { + if _, found := t.alreadyConverted[typeOf]; found { // Already converted + return "", nil + } + t.logf(depth, "Converting type %s", typeOf.String()) + + t.alreadyConverted[typeOf] = true + + entityName := t.Prefix + typeOf.Name() + t.Suffix + result := "" + if t.CreateInterface { + result += fmt.Sprintf("interface %s {\n", entityName) + } else { + result += fmt.Sprintf("class %s {\n", entityName) + } + if !t.DontExport { + result = "export " + result + } + builder := typeScriptClassBuilder{ + types: t.kinds, + indent: t.Indent, + prefix: t.Prefix, + suffix: t.Suffix, + } + + fields := deepFields(typeOf) + for _, field := range fields { + isPtr := field.Type.Kind() == reflect.Ptr + if isPtr { + field.Type = field.Type.Elem() + } + jsonFieldName := t.getJSONFieldName(field, isPtr) + if len(jsonFieldName) == 0 || jsonFieldName == "-" { + continue + } + + var err error + fldOpts := t.getFieldOptions(typeOf, field) + if fldOpts.TSTransform != "" { + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } else if _, isEnum := t.enums[field.Type]; isEnum { + t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name) + builder.AddEnumField(jsonFieldName, field) + } else if fldOpts.TSType != "" { // Struct: + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } else if field.Type.Kind() == reflect.Struct { // Struct: + t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) + typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + builder.AddStructField(jsonFieldName, field) + } else if field.Type.Kind() == reflect.Map { + t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name) + // Also convert map key types if needed + var keyTypeToConvert reflect.Type + switch field.Type.Key().Kind() { + case reflect.Struct: + keyTypeToConvert = field.Type.Key() + case reflect.Ptr: + keyTypeToConvert = field.Type.Key().Elem() + } + if keyTypeToConvert != nil { + typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + } + // Also convert map value types if needed + var valueTypeToConvert reflect.Type + switch field.Type.Elem().Kind() { + case reflect.Struct: + valueTypeToConvert = field.Type.Elem() + case reflect.Ptr: + valueTypeToConvert = field.Type.Elem().Elem() + } + if valueTypeToConvert != nil { + typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + } + + builder.AddMapField(jsonFieldName, field) + } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice: + if field.Type.Elem().Kind() == reflect.Ptr { //extract ptr type + field.Type = field.Type.Elem() + } + + arrayDepth := 1 + for field.Type.Elem().Kind() == reflect.Slice { // Slice of slices: + field.Type = field.Type.Elem() + arrayDepth++ + } + + if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs: + t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) + typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth) + } else { // Slice of simple fields: + t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts) + } + } else { // Simple field: + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } + if err != nil { + return "", err + } + } + + if t.CreateFromMethod { + t.CreateConstructor = true + } + + result += strings.Join(builder.fields, "\n") + "\n" + if !t.CreateInterface { + constructorBody := strings.Join(builder.constructorBody, "\n") + needsConvertValue := strings.Contains(constructorBody, "this.convertValues") + if t.CreateFromMethod { + result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent) + result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName) + result += fmt.Sprintf("%s}\n", t.Indent) + } + if t.CreateConstructor { + result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent) + result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n" + result += constructorBody + "\n" + result += fmt.Sprintf("%s}\n", t.Indent) + } + if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) { + result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n" + } + } + + if customCode != nil { + code := customCode[entityName] + if len(code) != 0 { + result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n" + } + } + + result += "}" + + return result, nil +} + +func (t *TypeScriptify) AddImport(i string) { + for _, cimport := range t.customImports { + if cimport == i { + return + } + } + + t.customImports = append(t.customImports, i) +} + +type typeScriptClassBuilder struct { + types map[reflect.Kind]string + indent string + fields []string + createFromMethodBody []string + constructorBody []string + prefix, suffix string +} + +func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error { + fieldType, kind := field.Type.Elem().Name(), field.Type.Elem().Kind() + typeScriptType := t.types[kind] + + if len(fieldName) > 0 { + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + if len(opts.TSType) > 0 { + t.addField(fieldName, opts.TSType) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + return nil + } else if len(typeScriptType) > 0 { + t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth))) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + return nil + } + } + + return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) +} + +func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error { + fieldType, kind := field.Type.Name(), field.Type.Kind() + + typeScriptType := t.types[kind] + if len(opts.TSType) > 0 { + typeScriptType = opts.TSType + } + + if len(typeScriptType) > 0 && len(fieldName) > 0 { + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, typeScriptType) + if opts.TSTransform == "" { + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + } else { + val := fmt.Sprintf(`source["%s"]`, strippedFieldName) + expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1) + t.addInitializerFieldLine(strippedFieldName, expression) + } + return nil + } + + return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) +} + +func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) { + fieldType := field.Type.Name() + t.addField(fieldName, t.prefix+fieldType+t.suffix) + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) +} + +func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField) { + fieldType := field.Type.Name() + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, t.prefix+fieldType+t.suffix) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) +} + +func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) { + fieldType := field.Type.Elem().Name() + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth))) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) +} + +func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) { + t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result.", fld, " = ", initializer, ";")) + t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this.", fld, " = ", initializer, ";")) +} + +func (t *typeScriptClassBuilder) addField(fld, fldType string) { + t.fields = append(t.fields, fmt.Sprint(t.indent, fld, ": ", fldType, ";")) +} + +func indentLines(str string, i int) string { + lines := strings.Split(str, "\n") + for n := range lines { + lines[n] = strings.Repeat("\t", i) + lines[n] + } + return strings.Join(lines, "\n") +}