From 182f43004a9173613cd12bf595681ab763468178 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 16 Dec 2023 14:02:19 +1100 Subject: [PATCH] Improved bindings generation: support JS Models. --- v3/internal/flags/bindings.go | 1 + v3/internal/parser/models.go | 9 +- v3/internal/parser/models_test.go | 95 +++++++++++++++---- v3/internal/parser/parser.go | 37 +++++++- v3/internal/parser/templates/model.js.tmpl | 41 ++++++++ v3/internal/parser/testdata/enum/main.go | 1 + v3/internal/parser/testdata/enum/models.js | 41 ++++++++ .../function_from_imported_package/models.js | 63 ++++++++++++ .../struct_literal_multiple_other/models.js | 63 ++++++++++++ .../models.js | 80 ++++++++++++++++ .../models.ts | 95 +++++++++---------- 11 files changed, 451 insertions(+), 75 deletions(-) create mode 100644 v3/internal/parser/templates/model.js.tmpl create mode 100644 v3/internal/parser/testdata/enum/models.js create mode 100644 v3/internal/parser/testdata/function_from_imported_package/models.js create mode 100644 v3/internal/parser/testdata/struct_literal_multiple_other/models.js create mode 100644 v3/internal/parser/testdata/struct_literal_non_pointer_single/models.js diff --git a/v3/internal/flags/bindings.go b/v3/internal/flags/bindings.go index 066ebcd9a..4f287461f 100644 --- a/v3/internal/flags/bindings.go +++ b/v3/internal/flags/bindings.go @@ -3,6 +3,7 @@ package flags type GenerateBindingsOptions struct { Silent bool `name:"silent" description:"Silent mode"` ModelsFilename string `name:"m" description:"The filename for the models file" default:"models.ts"` + TS bool `name:"ts" description:"Generate Typescript bindings"` TSPrefix string `description:"The prefix for the typescript names" default:""` TSSuffix string `description:"The postfix for the typescript names" default:""` UseInterfaces bool `name:"i" description:"Use interfaces instead of classes"` diff --git a/v3/internal/parser/models.go b/v3/internal/parser/models.go index 8ec7a498e..f6107f162 100644 --- a/v3/internal/parser/models.go +++ b/v3/internal/parser/models.go @@ -20,9 +20,12 @@ type ModelDefinitions struct { } func GenerateModel(wr io.Writer, def *ModelDefinitions, options *flags.GenerateBindingsOptions) error { - templateName := "model.ts.tmpl" - if options.UseInterfaces { - templateName = "interfaces.ts.tmpl" + templateName := "model.js.tmpl" + if options.TS { + templateName = "model.ts.tmpl" + if options.UseInterfaces { + templateName = "interfaces.ts.tmpl" + } } // Fix up TS names diff --git a/v3/internal/parser/models_test.go b/v3/internal/parser/models_test.go index 2020968cb..db87f299f 100644 --- a/v3/internal/parser/models_test.go +++ b/v3/internal/parser/models_test.go @@ -12,53 +12,103 @@ import ( func TestGenerateModels(t *testing.T) { tests := []struct { - dir string - want string - useInterface bool + name string + dir string + want string + useInterface bool + useTypescript bool }{ { - dir: "testdata/function_single", + name: "function single", + dir: "testdata/function_single", + useTypescript: true, }, { + name: "function from imported package", + dir: "testdata/function_from_imported_package", + want: getFile("testdata/function_from_imported_package/models.ts"), + useTypescript: true, + }, + { + name: "function from imported package (Javascript)", dir: "testdata/function_from_imported_package", - want: getFile("testdata/function_from_imported_package/models.ts"), + want: getFile("testdata/function_from_imported_package/models.js"), }, { - dir: "testdata/variable_single", + name: "variable single", + dir: "testdata/variable_single", + useTypescript: true, }, { - dir: "testdata/variable_single_from_function", + name: "variable single from function", + dir: "testdata/variable_single_from_function", + useTypescript: true, }, { - dir: "testdata/variable_single_from_other_function", - want: getFile("testdata/variable_single_from_other_function/models.ts"), + name: "variable single from other function", + dir: "testdata/variable_single_from_other_function", + want: getFile("testdata/variable_single_from_other_function/models.ts"), + useTypescript: true, }, { - dir: "testdata/struct_literal_single", - want: getFile("testdata/struct_literal_single/models.ts"), + name: "struct literal single", + dir: "testdata/struct_literal_single", + want: getFile("testdata/struct_literal_single/models.ts"), + useTypescript: true, }, { - dir: "testdata/struct_literal_multiple", + name: "struct literal multiple", + dir: "testdata/struct_literal_multiple", + useTypescript: true, }, { + name: "struct literal multiple other", + dir: "testdata/struct_literal_multiple_other", + want: getFile("testdata/struct_literal_multiple_other/models.ts"), + useTypescript: true, + }, + { + name: "struct literal multiple other (Javascript)", dir: "testdata/struct_literal_multiple_other", - want: getFile("testdata/struct_literal_multiple_other/models.ts"), + want: getFile("testdata/struct_literal_multiple_other/models.js"), }, { - dir: "testdata/struct_literal_multiple_files", + name: "struct literal non pointer single (Javascript)", + dir: "testdata/struct_literal_non_pointer_single", + want: getFile("testdata/struct_literal_non_pointer_single/models.ts"), + useTypescript: true, }, { + name: "struct literal non pointer single (Javascript)", + dir: "testdata/struct_literal_non_pointer_single", + want: getFile("testdata/struct_literal_non_pointer_single/models.js"), + }, + { + name: "struct literal multiple files", + dir: "testdata/struct_literal_multiple_files", + useTypescript: true, + }, + { + name: "enum", + dir: "testdata/enum", + want: getFile("testdata/enum/models.ts"), + useTypescript: true, + }, + { + name: "enum (Javascript)", dir: "testdata/enum", - want: getFile("testdata/enum/models.ts"), + want: getFile("testdata/enum/models.js"), }, { - dir: "testdata/enum-interface", - want: getFile("testdata/enum-interface/models.ts"), - useInterface: true, + name: "enum interface", + dir: "testdata/enum-interface", + want: getFile("testdata/enum-interface/models.ts"), + useInterface: true, + useTypescript: true, }, } for _, tt := range tests { - t.Run(tt.dir, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { // Run parser on directory project, err := ParseProject(tt.dir) if err != nil { @@ -68,6 +118,7 @@ func TestGenerateModels(t *testing.T) { // Generate Models got, err := GenerateModels(project.Models, project.Types, &flags.GenerateBindingsOptions{ UseInterfaces: tt.useInterface, + TS: tt.useTypescript, }) if err != nil { t.Fatalf("GenerateModels() error = %v", err) @@ -76,7 +127,11 @@ func TestGenerateModels(t *testing.T) { got = convertLineEndings(got) want := convertLineEndings(tt.want) if diff := cmp.Diff(want, got); diff != "" { - err = os.WriteFile(filepath.Join(tt.dir, "models.got.ts"), []byte(got), 0644) + gotFilename := "models.got.js" + if tt.useTypescript { + gotFilename = "models.got.ts" + } + err = os.WriteFile(filepath.Join(tt.dir, gotFilename), []byte(got), 0644) if err != nil { t.Errorf("os.WriteFile() error = %v", err) return diff --git a/v3/internal/parser/parser.go b/v3/internal/parser/parser.go index 2189081d8..acbac3fd2 100644 --- a/v3/internal/parser/parser.go +++ b/v3/internal/parser/parser.go @@ -181,6 +181,39 @@ func (f *Field) JSDef(pkg string) string { return result } +func (f *Field) JSDocType(pkg string) string { + var jsType string + switch f.Type.Name { + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64": + jsType = "number" + case "string": + jsType = "string" + case "bool": + jsType = "boolean" + default: + jsType = f.Type.Name + } + + var result string + isExternalStruct := f.Type.Package != "" && f.Type.Package != pkg && f.Type.IsStruct + if f.Type.Package == "" || f.Type.Package == pkg || !isExternalStruct { + if f.Type.IsStruct || f.Type.IsEnum { + result = fmt.Sprintf("%s.%s", pkg, jsType) + } else { + result = jsType + } + } else { + parts := strings.Split(f.Type.Package, "/") + result += fmt.Sprintf("%s.%s", parts[len(parts)-1], jsType) + } + + if !ast.IsExported(f.Name) { + result += " // Warning: this is unexported in the Go struct." + } + + return result +} + func (f *Field) DefaultValue() string { // Return the default value of the typescript version of the type as a string switch f.Type.Name { @@ -699,8 +732,8 @@ func (p *Project) getStructDef(name string, pkg *ParsedPackage) bool { if structType, ok := typeSpec.Type.(*ast.StructType); ok { if typeSpec.Name.Name == name { result := &StructDef{ - Name: name, - DocComment: typeDecl.Doc.Text(), + Name: name, + //TODO DocComment: CommentGroupToText(typeDecl.Doc), } pkg.StructCache[name] = result result.Fields = p.parseStructFields(structType, pkg) diff --git a/v3/internal/parser/templates/model.js.tmpl b/v3/internal/parser/templates/model.js.tmpl new file mode 100644 index 000000000..e204a9044 --- /dev/null +++ b/v3/internal/parser/templates/model.js.tmpl @@ -0,0 +1,41 @@ +{{$pkg := .Package}} +// Defining the {{$pkg}} namespace +export const {{$pkg}} = {}; +{{range $enumindex, $enumdef := .Enums}} +// Simulating the enum with an object +{{$pkg}}.{{$enumdef.Name}} = { + {{- range $constindex, $constdef := .Consts}} + {{- if $constdef.DocComment}} + // {{$constdef.DocComment}} + {{- end}} + {{$constdef.Name}}: {{$constdef.Value}},{{end}} +}; +{{- end}} +{{range $name, $def := .Models}} +{{- if $def.DocComment}} +// {{$def.DocComment}} +{{- end -}} +{{$pkg}}.{{$def.Name}} = class { + /** + * Creates a new {{$def.Name}} instance. + * @constructor + * @param {Object} source - The source object to create the {{$def.Name}}. +{{- range $field := $def.Fields}} + * @param { {{- .JSDocType $pkg -}} } source.{{$field.Name}}{{end}} + */ + constructor(source = {}) { + const { {{$def.DefaultValueList}} } = source; {{range $def.Fields}} + this.{{.JSName}} = {{.JSName}};{{end}} + } + + /** + * Creates a new {{$def.Name}} instance from a string or object. + * @param {string|object} source - The source data to create a {{$def.Name}} instance from. + * @returns {{$pkg}}.{{$def.Name}} A new {{$def.Name}} instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new {{$pkg}}.{{$def.Name}}(parsedSource); + } +}; +{{end}} diff --git a/v3/internal/parser/testdata/enum/main.go b/v3/internal/parser/testdata/enum/main.go index e4677ec70..d332966ed 100644 --- a/v3/internal/parser/testdata/enum/main.go +++ b/v3/internal/parser/testdata/enum/main.go @@ -25,6 +25,7 @@ type GreetService struct { target *Person } +// Person represents a person type Person struct { Title Title Name string diff --git a/v3/internal/parser/testdata/enum/models.js b/v3/internal/parser/testdata/enum/models.js new file mode 100644 index 000000000..28f774a0d --- /dev/null +++ b/v3/internal/parser/testdata/enum/models.js @@ -0,0 +1,41 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// Defining the main namespace +export const main = {}; + +// Simulating the enum with an object +main.Title = { + // Mister is a title + Mister: "Mr", + Miss: "Miss", + Ms: "Ms", + Mrs: "Mrs", + Dr: "Dr", +}; +main.Person = class { + /** + * Creates a new Person instance. + * @constructor + * @param {Object} source - The source object to create the Person. + * @param {main.Title} source.Title + * @param {string} source.Name + */ + constructor(source = {}) { + const { title = null, name = "" } = source; + this.title = title; + this.name = name; + } + + /** + * Creates a new Person instance from a string or object. + * @param {string|object} source - The source data to create a Person instance from. + * @returns main.Person A new Person instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new main.Person(parsedSource); + } +}; + diff --git a/v3/internal/parser/testdata/function_from_imported_package/models.js b/v3/internal/parser/testdata/function_from_imported_package/models.js new file mode 100644 index 000000000..94fe6bc9b --- /dev/null +++ b/v3/internal/parser/testdata/function_from_imported_package/models.js @@ -0,0 +1,63 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// Defining the main namespace +export const main = {}; + +main.Person = class { + /** + * Creates a new Person instance. + * @constructor + * @param {Object} source - The source object to create the Person. + * @param {string} source.Name + * @param {services.Address} source.Address + */ + constructor(source = {}) { + const { name = "", address = null } = source; + this.name = name; + this.address = address; + } + + /** + * Creates a new Person instance from a string or object. + * @param {string|object} source - The source data to create a Person instance from. + * @returns main.Person A new Person instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new main.Person(parsedSource); + } +}; + + +// Defining the services namespace +export const services = {}; + +services.Address = class { + /** + * Creates a new Address instance. + * @constructor + * @param {Object} source - The source object to create the Address. + * @param {string} source.Street + * @param {string} source.State + * @param {string} source.Country + */ + constructor(source = {}) { + const { street = "", state = "", country = "" } = source; + this.street = street; + this.state = state; + this.country = country; + } + + /** + * Creates a new Address instance from a string or object. + * @param {string|object} source - The source data to create a Address instance from. + * @returns services.Address A new Address instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new services.Address(parsedSource); + } +}; + diff --git a/v3/internal/parser/testdata/struct_literal_multiple_other/models.js b/v3/internal/parser/testdata/struct_literal_multiple_other/models.js new file mode 100644 index 000000000..94fe6bc9b --- /dev/null +++ b/v3/internal/parser/testdata/struct_literal_multiple_other/models.js @@ -0,0 +1,63 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// Defining the main namespace +export const main = {}; + +main.Person = class { + /** + * Creates a new Person instance. + * @constructor + * @param {Object} source - The source object to create the Person. + * @param {string} source.Name + * @param {services.Address} source.Address + */ + constructor(source = {}) { + const { name = "", address = null } = source; + this.name = name; + this.address = address; + } + + /** + * Creates a new Person instance from a string or object. + * @param {string|object} source - The source data to create a Person instance from. + * @returns main.Person A new Person instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new main.Person(parsedSource); + } +}; + + +// Defining the services namespace +export const services = {}; + +services.Address = class { + /** + * Creates a new Address instance. + * @constructor + * @param {Object} source - The source object to create the Address. + * @param {string} source.Street + * @param {string} source.State + * @param {string} source.Country + */ + constructor(source = {}) { + const { street = "", state = "", country = "" } = source; + this.street = street; + this.state = state; + this.country = country; + } + + /** + * Creates a new Address instance from a string or object. + * @param {string|object} source - The source data to create a Address instance from. + * @returns services.Address A new Address instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new services.Address(parsedSource); + } +}; + diff --git a/v3/internal/parser/testdata/struct_literal_non_pointer_single/models.js b/v3/internal/parser/testdata/struct_literal_non_pointer_single/models.js new file mode 100644 index 000000000..2b6af5068 --- /dev/null +++ b/v3/internal/parser/testdata/struct_literal_non_pointer_single/models.js @@ -0,0 +1,80 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// Defining the main namespace +export const main = {}; + +main.Person = class { + /** + * Creates a new Person instance. + * @constructor + * @param {Object} source - The source object to create the Person. + * @param {string} source.Name + * @param {main.Person} source.Parent + * @param {main.anon1} source.Details + */ + constructor(source = {}) { + const { name = "", parent = null, details = null } = source; + this.name = name; + this.parent = parent; + this.details = details; + } + + /** + * Creates a new Person instance from a string or object. + * @param {string|object} source - The source data to create a Person instance from. + * @returns main.Person A new Person instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new main.Person(parsedSource); + } +}; +main.anon1 = class { + /** + * Creates a new anon1 instance. + * @constructor + * @param {Object} source - The source object to create the anon1. + * @param {number} source.Age + * @param {main.anon2} source.Address + */ + constructor(source = {}) { + const { age = 0, address = null } = source; + this.age = age; + this.address = address; + } + + /** + * Creates a new anon1 instance from a string or object. + * @param {string|object} source - The source data to create a anon1 instance from. + * @returns main.anon1 A new anon1 instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new main.anon1(parsedSource); + } +}; +main.anon2 = class { + /** + * Creates a new anon2 instance. + * @constructor + * @param {Object} source - The source object to create the anon2. + * @param {string} source.Street + */ + constructor(source = {}) { + const { street = "" } = source; + this.street = street; + } + + /** + * Creates a new anon2 instance from a string or object. + * @param {string|object} source - The source data to create a anon2 instance from. + * @returns main.anon2 A new anon2 instance. + */ + static createFrom(source = {}) { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new main.anon2(parsedSource); + } +}; + diff --git a/v3/internal/parser/testdata/struct_literal_non_pointer_single/models.ts b/v3/internal/parser/testdata/struct_literal_non_pointer_single/models.ts index 91bab56ac..1244c2330 100644 --- a/v3/internal/parser/testdata/struct_literal_non_pointer_single/models.ts +++ b/v3/internal/parser/testdata/struct_literal_non_pointer_single/models.ts @@ -3,62 +3,57 @@ // This file is automatically generated. DO NOT EDIT export namespace main { - - export class Person { - name: string; - parent: Person; - details: anon1; - static createFrom(source: any = {}) { - return new Person(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) { - source = JSON.parse(source); - } - - this.name = source['name']; - this.parent = Person.createFrom(source['parent']); - this.details = anon1.createFrom(source['details']); - - } - } - - export class anon1 { - age: number; - address: anon2; - static createFrom(source: any = {}) { - return new anon1(source); - } + export class Person { + name: string; + parent: Person; + details: anon1; + + constructor(source: Partial = {}) { + const { name = "", parent = null, details = null } = source; + this.name = name; + this.parent = parent; + this.details = details; + } - constructor(source: any = {}) { - if ('string' === typeof source) { - source = JSON.parse(source); - } + static createFrom(source: string | object = {}): Person { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new Person(parsedSource as Partial); + } - this.age = source['age']; - this.address = anon2.createFrom(source['address']); - } - } - - export class anon2 { - street: string; - static createFrom(source: any = {}) { - return new anon2(source); - } + export class anon1 { + age: number; + address: anon2; + + constructor(source: Partial = {}) { + const { age = 0, address = null } = source; + this.age = age; + this.address = address; + } - constructor(source: any = {}) { - if ('string' === typeof source) { - source = JSON.parse(source); - } + static createFrom(source: string | object = {}): anon1 { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new anon1(parsedSource as Partial); + } - this.street = source['street']; - } - } - -} + + export class anon2 { + street: string; + + constructor(source: Partial = {}) { + const { street = "" } = source; + this.street = street; + } + + static createFrom(source: string | object = {}): anon2 { + let parsedSource = typeof source === 'string' ? JSON.parse(source) : source; + return new anon2(parsedSource as Partial); + } + + } + +} \ No newline at end of file