Support bindings, model & enum generation

This commit is contained in:
Lea Anthony
2023-12-12 21:51:40 +11:00
parent 43c4966873
commit 4b04c10f14
44 changed files with 3910 additions and 3126 deletions
+6 -11
View File
@@ -1,15 +1,10 @@
package commands
import "github.com/wailsapp/wails/v3/internal/parser"
import (
"github.com/wailsapp/wails/v3/internal/flags"
"github.com/wailsapp/wails/v3/internal/parser"
)
type GenerateBindingsOptions struct {
Silent bool `name:"silent" description:"Silent mode"`
ModelsFilename string `name:"m" description:"The filename for the models file" default:"models.ts"`
BindingsFilename string `name:"b" description:"The filename for the bindings file" default:"bindings_<package>.js"`
ProjectDirectory string `name:"p" description:"The project directory" default:"."`
OutputDirectory string `name:"d" description:"The output directory" default:"."`
}
func GenerateBindings(options *GenerateBindingsOptions) error {
return parser.GenerateBindingsAndModels(options.ProjectDirectory, options.OutputDirectory)
func GenerateBindings(options *flags.GenerateBindingsOptions) error {
return parser.GenerateBindingsAndModels(options)
}
+11
View File
@@ -0,0 +1,11 @@
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"`
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"`
ProjectDirectory string `name:"p" description:"The project directory" default:"."`
OutputDirectory string `name:"d" description:"The output directory" default:"frontend/bindings"`
}
+3
View File
@@ -58,6 +58,9 @@ This package contains the static analyser used for parsing Wails projects so tha
- [ ] Maps
- [x] Structs
- [ ] Generation of bindings
- [ ] Classes
- [ ] Interfaces
- [ ] Enums
## Limitations
+17 -22
View File
@@ -19,12 +19,18 @@ const bindingTemplate = `
/**
* {{structName}}.{{methodName}}
*Comments
* @param name {string}
* @param names {string}
* @returns {Promise<string>}
**/
{{methodName}}: function({{inputs}}) { return wails.CallByID({{ID}}, ...Array.prototype.slice.call(arguments, 0)); },
`
const enumTemplate = `
export enum {{.EnumName}} {
{{.EnumValues}}
}
`
var reservedWords = []string{
"abstract",
"arguments",
@@ -119,18 +125,19 @@ func GenerateBinding(structName string, method *BoundMethod) (string, []string,
pkgName := getPackageName(input)
if pkgName != "" {
models = append(models, pkgName)
if input.Type.IsStruct {
nsStruct := input.NamespacedStructType()
namespacedStructs = append(namespacedStructs, nsStruct)
}
}
params += " * @param " + inputName + " {" + input.JSType() + "}\n"
if input.Type.IsStruct || input.Type.IsEnum {
nsStruct := input.NamespacedStructType()
namespacedStructs = append(namespacedStructs, nsStruct)
}
params += " * @param " + inputName + " {" + input.JSType() + "}\n"
}
params = strings.TrimSuffix(params, "\n")
if len(params) == 0 {
params = " *"
params = " *"
}
result = strings.ReplaceAll(result, " * @param name {string}", params)
result = strings.ReplaceAll(result, "* @param names {string}", params)
var inputs string
for _, input := range method.Inputs {
pkgName := getPackageName(input)
@@ -232,19 +239,8 @@ func GenerateBindings(bindings map[string]map[string][]*BoundMethod) map[string]
packageBindings := bindings[packageName]
structNames := lo.Keys(packageBindings)
sort.Strings(structNames)
result[normalisedPackageNames[packageName]] += `
window.go = window.go || {};
`
// Iterate over the sorted struct keys
result[normalisedPackageNames[packageName]] += "window.go." + normalisedPackageNames[packageName] + " = {\n"
for _, structName := range structNames {
/**
* The GreetService provides methods to greet a person.
*/
//result[normalisedPackageNames[packageName]] += " /**\n"
//result[normalisedPackageNames[packageName]] += " {{structcomments}}\n"
//result[normalisedPackageNames[packageName]] += " */\n"
result[normalisedPackageNames[packageName]] += " " + structName + ": {\n"
result[normalisedPackageNames[packageName]] += "export const " + structName + " = {\n"
methods := packageBindings[structName]
sort.Slice(methods, func(i, j int) bool {
return methods[i].Name < methods[j].Name
@@ -255,9 +251,8 @@ window.go = window.go || {};
allModels = append(allModels, models...)
result[normalisedPackageNames[packageName]] += thisBinding
}
result[normalisedPackageNames[packageName]] += " },\n"
result[normalisedPackageNames[packageName]] += "};\n\n"
}
result[normalisedPackageNames[packageName]] += "};\n"
if len(allNamespacedStructs) > 0 {
typedefs := "/**\n"
+6
View File
@@ -27,6 +27,12 @@ func TestGenerateBindings(t *testing.T) {
dir string
want map[string]string
}{
{
"testdata/enum",
map[string]string{
"main": getFile("testdata/enum/bindings_main.js"),
},
},
{
"testdata/function_single",
map[string]string{
+19 -116
View File
@@ -3,6 +3,7 @@ package parser
import (
"bytes"
"embed"
"github.com/wailsapp/wails/v3/internal/flags"
"io"
"sort"
"strings"
@@ -15,16 +16,27 @@ var templates embed.FS
type ModelDefinitions struct {
Package string
Models map[string]*StructDef
Enums map[string]*TypeDef
}
func GenerateModel(wr io.Writer, def *ModelDefinitions) error {
tmpl, err := template.New("model.ts.tmpl").ParseFS(templates, "templates/model.ts.tmpl")
func GenerateModel(wr io.Writer, def *ModelDefinitions, options *flags.GenerateBindingsOptions) error {
templateName := "model.ts.tmpl"
if options.UseInterfaces {
templateName = "interfaces.ts.tmpl"
}
// Fix up TS names
for _, model := range def.Models {
model.Name = options.TSPrefix + model.Name + options.TSSuffix
}
tmpl, err := template.New(templateName).ParseFS(templates, "templates/"+templateName)
if err != nil {
println("Unable to create class template: " + err.Error())
return err
}
err = tmpl.ExecuteTemplate(wr, "model.ts.tmpl", def)
err = tmpl.ExecuteTemplate(wr, templateName, def)
if err != nil {
println("Problem executing template: " + err.Error())
return err
@@ -42,7 +54,7 @@ func pkgAlias(fullPkg string) string {
return pkgParts[len(pkgParts)-1]
}
func GenerateModels(models map[packagePath]map[structName]*StructDef) (string, error) {
func GenerateModels(models map[packagePath]map[structName]*StructDef, enums map[packagePath]map[string]*TypeDef, options *flags.GenerateBindingsOptions) (string, error) {
if models == nil {
return "", nil
}
@@ -53,7 +65,7 @@ func GenerateModels(models map[packagePath]map[structName]*StructDef) (string, e
// sort pkgs by alias (e.g. services) instead of full pkg name (e.g. github.com/wailsapp/wails/somedir/services)
// and then sort resulting list by the alias
var keys []string
for pkg, _ := range models {
for pkg := range models {
keys = append(keys, pkg)
}
@@ -65,120 +77,11 @@ func GenerateModels(models map[packagePath]map[structName]*StructDef) (string, e
err := GenerateModel(&buffer, &ModelDefinitions{
Package: pkgAlias(pkg),
Models: models[pkg],
})
Enums: enums[pkg],
}, options)
if err != nil {
return "", err
}
}
return buffer.String(), nil
}
//func GenerateClass(wr io.Writer, def *StructDef) error {
// tmpl, err := template.New("class.ts.tmpl").ParseFiles("templates/class.ts.tmpl")
// if err != nil {
// println("Unable to create class template: " + err.Error())
// return err
// }
//
// err = tmpl.ExecuteTemplate(wr, "class.ts.tmpl", def)
// if err != nil {
// println("Problem executing template: " + err.Error())
// return err
// }
// return nil
//}
//
//import (
// "bytes"
// "fmt"
// "go/ast"
// "go/types"
// "sort"
// "strings"
// "unicode"
//)
//
//func GenerateModels(context *Context) ([]byte, error) {
// var buf bytes.Buffer
// var pkgs []Package
// specs := context.GetBoundStructs()
// for pkg, pkgSpecs := range specs {
// pkgs = append(pkgs, Package{Name: pkg, Specs: pkgSpecs})
// }
// knownStructs := newAllModels(specs)
// sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Name < pkgs[j].Name })
// for _, pkg := range pkgs {
// if _, err := fmt.Fprintf(&buf, "namespace %s {\n", pkg.Name); err != nil {
// return nil, err
// }
// sort.Slice(pkg.Specs, func(i, j int) bool { return pkg.Specs[i].Name.Name < pkg.Specs[j].Name.Name })
// for _, spec := range pkg.Specs {
// if structType, ok := spec.Type.(*ast.StructType); ok {
// if _, err := fmt.Fprintf(&buf, " class %s {\n", spec.Name.Name); err != nil {
// return nil, err
// }
//
// for _, field := range structType.Fields.List {
//
// // Ignore field names that have a lower case first letter
// if !unicode.IsUpper(rune(field.Names[0].Name[0])) {
// continue
// }
//
// // Get the Go type of the field
// goType := types.ExprString(field.Type)
// // Check if the type is an array
// if arrayType, ok := field.Type.(*ast.ArrayType); ok {
// // Get the element type of the array
// elementType := types.ExprString(arrayType.Elt)
// // Look up the corresponding TypeScript type
// tsType, ok := goToTS[elementType]
// if !ok {
// // strip off the * prefix if it is there
// if strings.HasPrefix(elementType, "*") {
// elementType = elementType[1:]
// }
// if knownStructs.exists(elementType) {
// tsType = elementType
// } else {
// tsType = "any"
// }
// }
// // Output the field as an array of the corresponding TypeScript type
// if _, err := fmt.Fprintf(&buf, " %s: %s[];\n", field.Names[0].Name, tsType); err != nil {
// return nil, err
// }
// } else {
// // strip off the * prefix if it is there
// if strings.HasPrefix(goType, "*") {
// goType = goType[1:]
// }
// // Look up the corresponding TypeScript type
// tsType, ok := goToTS[goType]
// if !ok {
// if knownStructs.exists(goType) {
// tsType = goType
// } else {
// tsType = "any"
// }
// }
// // Output the field as the corresponding TypeScript type
// if _, err := fmt.Fprintf(&buf, " %s: %s;\n", field.Names[0].Name, tsType); err != nil {
// return nil, err
// }
// }
// }
//
// if _, err := fmt.Fprintf(&buf, " }\n"); err != nil {
// return nil, err
// }
// }
// }
//
// if _, err := fmt.Fprintf(&buf, "}\n\n"); err != nil {
// return nil, err
// }
// }
// return buf.Bytes(), nil
//}
+19 -19
View File
@@ -2,6 +2,7 @@ package parser
import (
"github.com/google/go-cmp/cmp"
"github.com/wailsapp/wails/v3/internal/flags"
"os"
"path/filepath"
"strings"
@@ -15,40 +16,39 @@ func TestGenerateModels(t *testing.T) {
want string
}{
{
"testdata/function_single",
"",
dir: "testdata/function_single",
},
{
"testdata/function_from_imported_package",
getFile("testdata/function_from_imported_package/models.ts"),
dir: "testdata/function_from_imported_package",
want: getFile("testdata/function_from_imported_package/models.ts"),
},
{
"testdata/variable_single",
"",
dir: "testdata/variable_single",
},
{
"testdata/variable_single_from_function",
"",
dir: "testdata/variable_single_from_function",
},
{
"testdata/variable_single_from_other_function",
getFile("testdata/variable_single_from_other_function/models.ts"),
dir: "testdata/variable_single_from_other_function",
want: getFile("testdata/variable_single_from_other_function/models.ts"),
},
{
"testdata/struct_literal_single",
getFile("testdata/struct_literal_single/models.ts"),
dir: "testdata/struct_literal_single",
want: getFile("testdata/struct_literal_single/models.ts"),
},
{
"testdata/struct_literal_multiple",
"",
dir: "testdata/struct_literal_multiple",
},
{
"testdata/struct_literal_multiple_other",
getFile("testdata/struct_literal_multiple_other/models.ts"),
dir: "testdata/struct_literal_multiple_other",
want: getFile("testdata/struct_literal_multiple_other/models.ts"),
},
{
"testdata/struct_literal_multiple_files",
"",
dir: "testdata/struct_literal_multiple_files",
},
{
dir: "testdata/enum",
want: getFile("testdata/enum/models.ts"),
},
}
for _, tt := range tests {
@@ -60,7 +60,7 @@ func TestGenerateModels(t *testing.T) {
}
// Generate Models
got, err := GenerateModels(project.Models)
got, err := GenerateModels(project.Models, project.Types, &flags.GenerateBindingsOptions{})
if err != nil {
t.Fatalf("GenerateModels() error = %v", err)
}
+247 -18
View File
@@ -2,6 +2,7 @@ package parser
import (
"fmt"
"github.com/wailsapp/wails/v3/internal/flags"
"go/ast"
"go/build"
"go/parser"
@@ -12,6 +13,7 @@ import (
"reflect"
"strconv"
"strings"
"time"
"github.com/samber/lo"
"github.com/wailsapp/wails/v3/internal/hash"
@@ -26,16 +28,39 @@ type StructDef struct {
Fields []*Field
}
func (s *StructDef) DefaultValueList() string {
var allFields []string
for _, field := range s.Fields {
thisFieldWithDefaultValue := fmt.Sprintf("%s = %s", field.JSName(), field.DefaultValue())
allFields = append(allFields, thisFieldWithDefaultValue)
}
return strings.Join(allFields, ", ")
}
type ParameterType struct {
Name string
IsStruct bool
IsSlice bool
IsPointer bool
IsEnum bool
MapKey *ParameterType
MapValue *ParameterType
Package string
}
type EnumDef struct {
Name string
Filename string
DocComments []string
Values []*EnumValue
}
type EnumValue struct {
Name string
Value string
DocComments []string
}
type Parameter struct {
Name string
Type *ParameterType
@@ -73,7 +98,7 @@ func (p *Parameter) JSType() string {
}
// if the type is a struct, we need to add the package name
if p.Type.IsStruct {
if p.Type.IsStruct || p.Type.IsEnum {
typeName = p.NamespacedStructType()
typeName = strings.ReplaceAll(typeName, ".", "")
}
@@ -117,7 +142,7 @@ func (f *Field) JSName() string {
// via assignment for simple types or constructors for structs
func (f *Field) TSBuild(pkg string) string {
if !f.Type.IsStruct {
return fmt.Sprintf("source['%s']", f.JSName())
return fmt.Sprintf("safeSource['%s']", f.JSName())
}
if f.Type.Package == "" || f.Type.Package == pkg {
@@ -141,7 +166,8 @@ func (f *Field) JSDef(pkg string) string {
}
var result string
if f.Type.Package == "" || f.Type.Package == pkg {
isExternalStruct := f.Type.Package != "" && f.Type.Package != pkg && f.Type.IsStruct
if f.Type.Package == "" || f.Type.Package == pkg || !isExternalStruct {
result += fmt.Sprintf("%s: %s;", f.JSName(), jsType)
} else {
parts := strings.Split(f.Type.Package, "/")
@@ -155,12 +181,45 @@ func (f *Field) JSDef(pkg string) string {
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 {
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uintptr", "float32", "float64", "uint64":
return "0"
case "string":
return `""`
case "bool":
return "false"
default:
return "null"
}
}
type ConstDef struct {
Name string
DocComment string
Value string
}
type TypeDef struct {
Name string
DocComments []string
Type string
Consts []*ConstDef
ShouldGenerate bool
}
func (t *TypeDef) GeneratedName() string {
return t.Name + "Enum"
}
type ParsedPackage struct {
Pkg *ast.Package
Name string
Path string
Dir string
StructCache map[structName]*StructDef
TypeCache map[string]*TypeDef
}
type Project struct {
@@ -168,7 +227,19 @@ type Project struct {
Path string
BoundMethods map[packagePath]map[structName][]*BoundMethod
Models map[packagePath]map[structName]*StructDef
Types map[packagePath]map[structName]*TypeDef
anonymousStructIDCounter int
Stats Stats
}
type Stats struct {
NumPackages int
NumStructs int
NumMethods int
NumEnums int
NumModels int
StartTime time.Time
EndTime time.Time
}
func ParseProject(projectPath string) (*Project, error) {
@@ -176,6 +247,7 @@ func ParseProject(projectPath string) (*Project, error) {
BoundMethods: make(map[packagePath]map[structName][]*BoundMethod),
packageCache: make(map[string]*ParsedPackage),
}
result.Stats.StartTime = time.Now()
pkgs, err := result.parseDirectory(projectPath)
if err != nil {
return nil, err
@@ -195,8 +267,8 @@ func ParseProject(projectPath string) (*Project, error) {
return result, nil
}
func GenerateBindingsAndModels(projectDir string, outputDir string) error {
p, err := ParseProject(projectDir)
func GenerateBindingsAndModels(options *flags.GenerateBindingsOptions) error {
p, err := ParseProject(options.ProjectDirectory)
if err != nil {
return err
}
@@ -204,37 +276,51 @@ func GenerateBindingsAndModels(projectDir string, outputDir string) error {
if p.BoundMethods == nil {
return nil
}
err = os.MkdirAll(outputDir, 0755)
err = os.MkdirAll(options.OutputDirectory, 0755)
if err != nil {
return err
}
p.Stats.NumMethods = len(p.BoundMethods)
generatedMethods := GenerateBindings(p.BoundMethods)
for pkg, text := range generatedMethods {
// Write the file
err = os.WriteFile(filepath.Join(outputDir, "bindings_"+pkg+".js"), []byte(text), 0644)
err = os.WriteFile(filepath.Join(options.OutputDirectory, pkg+".js"), []byte(text), 0644)
if err != nil {
return err
}
}
p.Stats.NumModels = len(p.Models)
p.Stats.NumEnums = len(p.Types)
// Generate Models
if len(p.Models) > 0 {
generatedModels, err := GenerateModels(p.Models)
generatedModels, err := GenerateModels(p.Models, p.Types, options)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(outputDir, "models.ts"), []byte(generatedModels), 0644)
err = os.WriteFile(filepath.Join(options.OutputDirectory, options.ModelsFilename), []byte(generatedModels), 0644)
if err != nil {
return err
}
}
absPath, err := filepath.Abs(projectDir)
p.Stats.EndTime = time.Now()
absPath, err := filepath.Abs(options.ProjectDirectory)
if err != nil {
return err
}
println("Generated bindings and models for project: " + absPath)
absPath, err = filepath.Abs(outputDir)
fmt.Printf("Processed: %s, %s, %s, %s, %s in %s.\n",
pluralise(p.Stats.NumPackages, "Package"),
pluralise(p.Stats.NumStructs, "Struct"),
pluralise(p.Stats.NumMethods, "Method"),
pluralise(p.Stats.NumEnums, "Enum"),
pluralise(p.Stats.NumModels, "Model"),
p.Stats.EndTime.Sub(p.Stats.StartTime).String())
absPath, err = filepath.Abs(options.OutputDirectory)
if err != nil {
return err
}
@@ -243,6 +329,13 @@ func GenerateBindingsAndModels(projectDir string, outputDir string) error {
return nil
}
func pluralise(number int, word string) string {
if number == 1 {
return fmt.Sprintf("%d %s", number, word)
}
return fmt.Sprintf("%d %ss", number, word)
}
func (p *Project) parseDirectory(dir string) (map[string]*ParsedPackage, error) {
if p.packageCache[dir] != nil {
return map[string]*ParsedPackage{dir: p.packageCache[dir]}, nil
@@ -261,6 +354,7 @@ func (p *Project) parseDirectory(dir string) (map[string]*ParsedPackage, error)
return nil, err
}
var result = make(map[string]*ParsedPackage)
p.Stats.NumPackages = len(pkgs)
for packageName, pkg := range pkgs {
parsedPackage := &ParsedPackage{
Pkg: pkg,
@@ -268,6 +362,7 @@ func (p *Project) parseDirectory(dir string) (map[string]*ParsedPackage, error)
Path: packageName,
Dir: getDirectoryForPackage(pkg),
StructCache: make(map[structName]*StructDef),
TypeCache: make(map[string]*TypeDef),
}
p.packageCache[packageName] = parsedPackage
result[packageName] = parsedPackage
@@ -285,6 +380,28 @@ func (p *Project) findApplicationNewCalls(pkgs map[string]*ParsedPackage) (err e
for _, file := range thisPackage.Files {
// Use an ast.Inspector to find the calls to application.New
ast.Inspect(file, func(n ast.Node) bool {
// Check for const declaration
genDecl, ok := n.(*ast.GenDecl)
if ok {
switch genDecl.Tok {
case token.TYPE:
var comments []string
if genDecl.Doc != nil {
comments = CommentGroupToText(genDecl.Doc)
}
for _, spec := range genDecl.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
p.parseTypeDeclaration(typeSpec, pkg, comments)
}
}
case token.CONST:
p.parseConstDeclaration(genDecl, pkg)
default:
}
return true
}
// Check if the node is a call expression
callExpr, ok := n.(*ast.CallExpr)
if !ok {
@@ -372,6 +489,7 @@ func (p *Project) findApplicationNewCalls(pkgs map[string]*ParsedPackage) (err e
return true
})
}
p.addTypes(pkg.Path, pkg.TypeCache)
if !callFound {
return fmt.Errorf("no Bound structs found")
}
@@ -443,7 +561,7 @@ func (p *Project) parseBoundStructMethods(name string, pkg *ParsedPackage) error
method := &BoundMethod{
ID: id,
Name: funcDecl.Name.Name,
DocComment: funcDecl.Doc.Text(),
DocComment: strings.TrimSpace(funcDecl.Doc.Text()),
Alias: alias,
}
@@ -464,6 +582,20 @@ func (p *Project) parseBoundStructMethods(name string, pkg *ParsedPackage) error
return nil
}
func (p *Project) addTypes(packagePath string, types map[string]*TypeDef) {
if types == nil || len(types) == 0 {
return
}
if p.Types == nil {
p.Types = make(map[string]map[string]*TypeDef)
}
_, ok := p.Types[packagePath]
if !ok {
p.Types[packagePath] = make(map[string]*TypeDef)
}
p.Types[packagePath] = types
}
func (p *Project) parseParameters(params *ast.FieldList, pkg *ParsedPackage) []*Parameter {
var result []*Parameter
for _, field := range params.List {
@@ -489,11 +621,21 @@ func (p *Project) parseParameters(params *ast.FieldList, pkg *ParsedPackage) []*
}
func (p *Project) parseParameterType(field *ast.Field, pkg *ParsedPackage) *ParameterType {
result := &ParameterType{}
result := &ParameterType{
Package: pkg.Path,
}
result.Name = getTypeString(field.Type)
switch t := field.Type.(type) {
case *ast.Ident:
result.IsStruct = isStructType(t)
if !result.IsStruct {
// Check if it's a type alias
typeDef, ok := pkg.TypeCache[t.Name]
if ok {
typeDef.ShouldGenerate = true
result.IsEnum = true
}
}
case *ast.StarExpr:
result = p.parseParameterType(&ast.Field{Type: t.X}, pkg)
result.IsPointer = true
@@ -597,9 +739,9 @@ func (p *Project) parseStructFields(structType *ast.StructType, pkg *ParsedPacka
if !ok {
p.getStructDef(paramType.Name, pkg)
}
if paramType.Package == "" {
paramType.Package = pkg.Path
}
}
if paramType.Package == "" {
paramType.Package = pkg.Path
}
thisField.Type = paramType
result = append(result, thisField)
@@ -822,6 +964,72 @@ func (p *Project) getFunctionFromName(name string, parsedPackage *ParsedPackage)
return nil, fmt.Errorf("function not found")
}
func (p *Project) parseTypeDeclaration(decl *ast.TypeSpec, pkg *ParsedPackage, comments []string) {
switch t := decl.Type.(type) {
case *ast.Ident:
switch t.Name {
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64",
"uintptr", "float32", "float64", "string", "bool":
// Store this in the type cache
pkg.TypeCache[decl.Name.Name] = &TypeDef{
Name: decl.Name.Name,
Type: t.Name,
DocComments: TrimSlice(comments),
}
}
}
}
func (p *Project) parseConstDeclaration(decl *ast.GenDecl, pkg *ParsedPackage) {
// Check if the type of the constant is in the type cache and if it doesn't exist, return
var latestValues []ast.Expr
for _, spec := range decl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
// Extract the Type
typeString := getTypeString(valueSpec.Type)
if typeString == "" {
continue
}
// Check if the type is in the type cache
typeDecl, ok := pkg.TypeCache[typeString]
if !ok {
continue
}
// Get the latest values
if len(valueSpec.Values) > 0 {
latestValues = valueSpec.Values
}
// Iterate over the names
for index, name := range valueSpec.Names {
constDecl := &ConstDef{
Name: name.Name,
Value: typeString,
}
// Get the value
if len(latestValues) > 0 {
switch t := latestValues[index].(type) {
case *ast.BasicLit:
constDecl.Value = t.Value
case *ast.Ident:
constDecl.Value = t.Name
}
}
if valueSpec.Doc != nil {
constDecl.DocComment = strings.TrimSpace(valueSpec.Doc.Text())
}
typeDecl.Consts = append(typeDecl.Consts, constDecl)
}
}
}
func getTypeString(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
@@ -852,7 +1060,10 @@ func isStructType(expr ast.Expr) bool {
case *ast.SliceExpr:
return isStructType(e.X)
case *ast.Ident:
return e.Obj != nil && e.Obj.Kind == ast.Typ
if e.Obj != nil && e.Obj.Kind == ast.Typ {
return isStructType(e.Obj.Decl.(*ast.TypeSpec).Type)
}
return false
default:
return false
}
@@ -869,3 +1080,21 @@ func getDirectoryForPackage(pkg *ast.Package) string {
}
return ""
}
func CommentGroupToText(comments *ast.CommentGroup) []string {
if comments == nil {
return nil
}
var result []string
for _, comment := range comments.List {
result = append(result, strings.TrimSpace(comment.Text))
}
return result
}
func TrimSlice(comments []string) []string {
for i, comment := range comments {
comments[i] = strings.TrimSpace(comment)
}
return comments
}
+162
View File
@@ -0,0 +1,162 @@
package parser
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestParseEnum(t *testing.T) {
tests := []struct {
name string
dir string
wantBoundMethods map[string]map[string][]*BoundMethod
wantEnums map[string]map[string]*EnumDef
wantModels map[string]map[string]*StructDef
wantTypes map[string]map[string]*TypeDef
wantErr bool
}{
{
name: "should find a bound services with an enum",
dir: "testdata/enum",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "Greet does XYZ",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
{
Name: "title",
Type: &ParameterType{
Package: "main",
Name: "Title",
IsEnum: true,
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
{
Name: "NewPerson",
DocComment: "NewPerson creates a new person",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Type: &ParameterType{
Package: "main",
Name: "Person",
IsStruct: true,
IsPointer: true,
},
},
},
ID: 1661412647,
},
},
},
},
wantTypes: map[string]map[string]*TypeDef{
"main": {
"Title": {
Name: "Title",
Type: "string",
Consts: []*ConstDef{
{
Name: "Mister",
DocComment: "Mister is a title",
Value: `"Mr"`,
},
{
Name: "Miss",
Value: `"Miss"`,
},
{
Name: "Ms",
Value: `"Ms"`,
},
{
Name: "Mrs",
Value: `"Mrs"`,
},
{
Name: "Dr",
Value: `"Dr"`,
},
},
ShouldGenerate: true,
},
},
},
wantModels: map[string]map[string]*StructDef{
"main": {
"Person": {
Name: "Person",
Fields: []*Field{
{
Name: "Title",
Type: &ParameterType{
Package: "main",
Name: "Title",
IsEnum: true,
},
},
{
Name: "Name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseProject(tt.dir)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
if !reflect.DeepEqual(tt.wantModels, got.Models) {
t.Errorf("ParseDirectory() failed:\n" + cmp.Diff(tt.wantModels, got.Models))
}
if diff := cmp.Diff(tt.wantTypes, got.Types); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
})
}
}
+204
View File
@@ -0,0 +1,204 @@
package parser
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestParseFunction(t *testing.T) {
tests := []struct {
name string
dir string
wantBoundMethods map[string]map[string][]*BoundMethod
wantEnums map[string]map[string]*EnumDef
wantModels map[string]map[string]*StructDef
wantTypes map[string]map[string]*TypeDef
wantErr bool
}{
{
name: "should find a bound service returned from a function call",
dir: "testdata/function_single",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "Greet someone",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
},
},
},
},
{
name: "should find a bound service returned from a function call in another package",
dir: "testdata/function_from_imported_package",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "Greet does XYZ",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
{
Name: "NewPerson",
DocComment: "NewPerson creates a new person",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "Person",
IsPointer: true,
IsStruct: true,
},
},
},
ID: 1661412647,
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services": {
"OtherService": {
{
Name: "Yay",
Outputs: []*Parameter{
{
Type: &ParameterType{
Name: "Address",
IsStruct: true,
IsPointer: true,
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
},
},
},
ID: 302702907,
},
},
},
},
wantModels: map[string]map[string]*StructDef{
"main": {
"Person": {
Name: "Person",
Fields: []*Field{
{
Name: "Name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
{
Name: "Address",
Type: &ParameterType{
Name: "Address",
IsStruct: true,
IsPointer: true,
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
},
},
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services": {
"Address": {
Name: "Address",
Fields: []*Field{
{
Name: "Street",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "string",
},
},
{
Name: "State",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "string",
},
},
{
Name: "Country",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "string",
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseProject(tt.dir)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
if !reflect.DeepEqual(tt.wantModels, got.Models) {
t.Errorf("ParseDirectory() failed:\n" + cmp.Diff(tt.wantModels, got.Models))
}
if diff := cmp.Diff(tt.wantTypes, got.Types); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
})
}
}
@@ -0,0 +1,250 @@
package parser
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestParseStructLiteralMultiple(t *testing.T) {
tests := []struct {
name string
dir string
wantBoundMethods map[string]map[string][]*BoundMethod
wantEnums map[string]map[string]*EnumDef
wantModels map[string]map[string]*StructDef
wantTypes map[string]map[string]*TypeDef
wantErr bool
}{
{
name: "should find multiple bound services",
dir: "testdata/struct_literal_multiple",
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
},
"OtherService": {
{
Name: "Hello",
ID: 4249972365,
},
},
},
},
wantErr: false,
},
{
name: "should find multiple bound services over multiple files",
dir: "testdata/struct_literal_multiple_files",
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
},
"OtherService": {
{
Name: "Hello",
ID: 4249972365,
},
},
},
},
wantErr: false,
},
{
name: "should find multiple bound services over multiple packages",
dir: "testdata/struct_literal_multiple_other",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "Greet does XYZ",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
{
Name: "NewPerson",
DocComment: "NewPerson creates a new person",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "Person",
IsPointer: true,
IsStruct: true,
},
},
},
ID: 1661412647,
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services": {
"OtherService": {
{
Name: "Yay",
Outputs: []*Parameter{
{
Type: &ParameterType{
Name: "Address",
IsStruct: true,
IsPointer: true,
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services",
},
},
},
ID: 469445984,
},
},
},
},
wantModels: map[string]map[string]*StructDef{
"main": {
"Person": {
Name: "Person",
Fields: []*Field{
{
Name: "Name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
{
Name: "Address",
Type: &ParameterType{
Name: "Address",
IsStruct: true,
IsPointer: true,
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services",
},
},
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services": {
"Address": {
Name: "Address",
Fields: []*Field{
{
Name: "Street",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services",
Name: "string",
},
},
{
Name: "State",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services",
Name: "string",
},
},
{
Name: "Country",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services",
Name: "string",
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseProject(tt.dir)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
if !reflect.DeepEqual(tt.wantModels, got.Models) {
t.Errorf("ParseDirectory() failed:\n" + cmp.Diff(tt.wantModels, got.Models))
}
if diff := cmp.Diff(tt.wantTypes, got.Types); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
})
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,238 @@
package parser
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestParseVariableSingle(t *testing.T) {
tests := []struct {
name string
dir string
wantBoundMethods map[string]map[string][]*BoundMethod
wantEnums map[string]map[string]*EnumDef
wantModels map[string]map[string]*StructDef
wantTypes map[string]map[string]*TypeDef
wantErr bool
}{
{
name: "should find a bound services using a variable",
dir: "testdata/variable_single",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "Greet someone",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
},
},
},
},
{
name: "should find a bound services using a variable from function call",
dir: "testdata/variable_single_from_function",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "Greet someone",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
},
},
},
},
{
name: "should find a bound services using a variable from function call in another package",
dir: "testdata/variable_single_from_other_function",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Name: "Greet",
DocComment: "Greet does XYZ",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
ID: 1411160069,
},
{
Name: "NewPerson",
DocComment: "NewPerson creates a new person",
Inputs: []*Parameter{
{
Name: "name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
},
Outputs: []*Parameter{
{
Name: "",
Type: &ParameterType{
Package: "main",
Name: "Person",
IsPointer: true,
IsStruct: true,
},
},
},
ID: 1661412647,
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services": {
"OtherService": {
{
Name: "Yay",
Outputs: []*Parameter{
{
Type: &ParameterType{
Name: "Address",
IsStruct: true,
IsPointer: true,
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
},
},
},
ID: 302702907,
},
},
},
},
wantModels: map[string]map[string]*StructDef{
"main": {
"Person": {
Name: "Person",
Fields: []*Field{
{
Name: "Name",
Type: &ParameterType{
Package: "main",
Name: "string",
},
},
{
Name: "Address",
Type: &ParameterType{
Name: "Address",
IsStruct: true,
IsPointer: true,
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
},
},
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services": {
"Address": {
Name: "Address",
Fields: []*Field{
{
Name: "Street",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "string",
},
},
{
Name: "State",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "string",
},
},
{
Name: "Country",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "string",
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseProject(tt.dir)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
if !reflect.DeepEqual(tt.wantModels, got.Models) {
t.Errorf("ParseDirectory() failed:\n" + cmp.Diff(tt.wantModels, got.Models))
}
if diff := cmp.Diff(tt.wantTypes, got.Types); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
})
}
}
@@ -1,16 +0,0 @@
export class {{.Name}} {
{{range .Fields}}{{.}}
{{end}}
static createFrom(source: any = {}) {
return new {{.Name}}(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) {
source = JSON.parse(source);
}
{{range .Fields}}this.{{jsName .}} = source["{{jsName .}}"]
{{end}}
}
}
@@ -0,0 +1,8 @@
{{$pkg := .Package}}
export namespace {{.Package}} {
{{range $name, $def := .Models}}
export interface {{$def.Name}} { {{range $def.Fields}}
{{.JSDef $pkg}}{{end}}
}
{{end}}
}
+28 -18
View File
@@ -1,21 +1,31 @@
{{$pkg := .Package}}
export namespace {{.Package}} {
{{range $name, $def := .Models}}
export class {{$def.Name}} {
{{range $def.Fields}}{{.JSDef $pkg}}
{{range $enumindex, $enumdef := .Enums}}
{{- range $commentindex, $commentdef := .DocComments}}
{{$commentdef -}}
{{- end}}
export enum {{$enumdef.Name}} {
{{- range $constindex, $constdef := .Consts}}
{{- if $constdef.DocComment}}
// {{$constdef.DocComment}}
{{- end}}
{{$constdef.Name}} = {{$constdef.Value}},{{end}}
}
{{- end}}
{{range $name, $def := .Models}}
export class {{$def.Name}} {
{{range $def.Fields}}{{.JSDef $pkg}}
{{end}}
constructor(source: Partial<{{$def.Name}}> = {}) {
const { {{$def.DefaultValueList}} } = source; {{range $def.Fields}}
this.{{.JSName}} = {{.JSName}};{{end}}
}
static createFrom(source: string | object = {}): {{$def.Name}} {
let parsedSource = typeof source === 'string' ? JSON.parse(source) : source;
return new {{$def.Name}}(parsedSource as Partial<{{$def.Name}}>);
}
}
{{end}}
static createFrom(source: any = {}) {
return new {{$def.Name}}(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) {
source = JSON.parse(source);
}
{{range $def.Fields}}this.{{.JSName}} = {{.TSBuild $pkg}};
{{end}}
}
}
{{end}}
}
}
+29
View File
@@ -0,0 +1,29 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* @typedef {import('./models').main.Title} mainTitle
* @typedef {import('./models').main.Person} mainPerson
*/
export const GreetService = {
/**
* GreetService.Greet
* Greet does XYZ
* @param name {string}
* @param title {mainTitle}
* @returns {Promise<string>}
**/
Greet: function(name, title) { return wails.CallByID(1411160069, ...Array.prototype.slice.call(arguments, 0)); },
/**
* GreetService.NewPerson
* NewPerson creates a new person
* @param name {string}
* @returns {Promise<mainPerson>}
**/
NewPerson: function(name) { return wails.CallByID(1661412647, ...Array.prototype.slice.call(arguments, 0)); },
};
+58
View File
@@ -0,0 +1,58 @@
package main
import (
_ "embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
type Title string
const (
// Mister is a title
Mister Title = "Mr"
Miss Title = "Miss"
Ms Title = "Ms"
Mrs Title = "Mrs"
Dr Title = "Dr"
)
// GreetService is great
type GreetService struct {
SomeVariable int
lowerCase string
target *Person
}
type Person struct {
Title Title
Name string
}
// Greet does XYZ
func (*GreetService) Greet(name string, title Title) string {
return "Hello " + string(title) + " " + name
}
// NewPerson creates a new person
func (*GreetService) NewPerson(name string) *Person {
return &Person{Name: name}
}
func main() {
app := application.New(application.Options{
Bind: []interface{}{
&GreetService{},
},
})
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

Some files were not shown because too many files have changed in this diff Show More