Skip to content

Commit

Permalink
Improved bindings generation: support JS Models.
Browse files Browse the repository at this point in the history
  • Loading branch information
leaanthony committed Dec 16, 2023
1 parent 23c2660 commit 182f430
Show file tree
Hide file tree
Showing 11 changed files with 448 additions and 72 deletions.
1 change: 1 addition & 0 deletions v3/internal/flags/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
9 changes: 6 additions & 3 deletions v3/internal/parser/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 75 additions & 20 deletions v3/internal/parser/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
{
name: "variable single",
dir: "testdata/variable_single",
useTypescript: true,
},
{
dir: "testdata/variable_single",
name: "variable single from function",
dir: "testdata/variable_single_from_function",
useTypescript: true,
},
{
dir: "testdata/variable_single_from_function",
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/variable_single_from_other_function",
want: getFile("testdata/variable_single_from_other_function/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_single",
want: getFile("testdata/struct_literal_single/models.ts"),
name: "struct literal multiple",
dir: "testdata/struct_literal_multiple",
useTypescript: true,
},
{
dir: "testdata/struct_literal_multiple",
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"),
},
{
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,
},
{
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.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 {
Expand All @@ -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)
Expand All @@ -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
Expand Down
37 changes: 35 additions & 2 deletions v3/internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions v3/internal/parser/templates/model.js.tmpl
Original file line number Diff line number Diff line change
@@ -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}}
1 change: 1 addition & 0 deletions v3/internal/parser/testdata/enum/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type GreetService struct {
target *Person
}

// Person represents a person
type Person struct {
Title Title
Name string
Expand Down
41 changes: 41 additions & 0 deletions v3/internal/parser/testdata/enum/models.js
Original file line number Diff line number Diff line change
@@ -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);
}
};

Original file line number Diff line number Diff line change
@@ -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);
}
};

Loading

0 comments on commit 182f430

Please sign in to comment.