Improved bindings generation: support JS Models.

This commit is contained in:
Lea Anthony 2023-12-16 14:02:19 +11:00
commit 182f43004a
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
11 changed files with 448 additions and 72 deletions

View file

@ -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"`

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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}}

View file

@ -25,6 +25,7 @@ type GreetService struct {
target *Person
}
// Person represents a person
type Person struct {
Title Title
Name string

View file

@ -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);
}
};

View file

@ -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);
}
};

View file

@ -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);
}
};

View file

@ -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);
}
};

View file

@ -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<Person> = {}) {
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<Person>);
}
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<anon1> = {}) {
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<anon1>);
}
this.street = source['street'];
}
}
}
export class anon2 {
street: string;
constructor(source: Partial<anon2> = {}) {
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<anon2>);
}
}
}