Update bindings generator to generate bindings in packages and files.

Remove unused JavaScript files
Update tests.
Update v3 docs
This commit is contained in:
Lea Anthony
2023-12-22 20:01:42 +11:00
parent cdf4bdd2ba
commit 2bb25b12ff
125 changed files with 4008 additions and 2158 deletions
+9 -34
View File
@@ -17,21 +17,16 @@ We also want to get all examples working on Linux.
#### How Can I Help?
If you are interested in helping out, please review the table below and look for
untested scenarios. The parser code and tests are located in `v3/internal/parser`.
All tests can be run using `go test ./...` from the `v3` directory.
You can generate bindings using the `wails3 generate bindings` command. This will generate bindings for all exported struct methods bound to your project.
You can then use these bindings in your frontend code. You can see an example of this in the `examples/bindings` directory.
Run `wails3 generate bindings -help` to view options that govern how bindings are generated.
Review the table below and look for untested scenarios. The parser code and tests are located in `v3/internal/parser`. All tests can be run using `go test ./...` from the `v3` directory.
Basically, try to break it and let us know if you find any issues! :smile:
#### Status
Bindings for struct (CallByID):
- [x] Same package
- [x] Different package
- [ ] Different package with same name
- [x] Containing another struct from same package
- [x] Containing another struct from different package
- [x] Containing an anonymous struct
- :material-check-bold: - Working
- :material-minus: - Partially working
@@ -40,12 +35,6 @@ Bindings for struct (CallByID):
{{ read_csv("alpha3-bindings-callbyid.csv") }}
Bindings for struct (CallByName):
- [ ] Same package
- [ ] Different package
- [ ] Different package with same name
- [ ] Containing another struct from same package
- [ ] Containing another struct from different package
- [ ] Containing an anonymous struct
- :material-check-bold: - Working
- :material-minus: - Partially working
@@ -54,32 +43,18 @@ Bindings for struct (CallByName):
{{ read_csv("alpha3-bindings-callbyname.csv") }}
Models:
- [x] Class model for struct in same package
- [x] Class model for struct in different package
- [ ] Interface model for struct in same package
- [ ] Interface model for struct in different package
- [x] Enum in same package
- [x] Enum in different package
- [x] Interface using enum in same package
- [ ] Interface using enum in different package
Examples:
- [ ] All examples working on Linux
- :material-check-bold: - Working
- :material-minus: - Partially working
- :material-close: - Not working
**Bindings**:
{{ read_csv("alpha3-bindings-callbyid.csv") }}
**Models**:
{{ read_csv("alpha3-models.csv") }}
Examples:
- [ ] All examples working on Linux
### Alpha 2
@@ -1,7 +1,7 @@
Scenario,Windows,Mac,Linux,WSL
Same package," "," "," "," "
Different package," "," "," "," "
Same package,:material-check-bold:," "," "," "
Different package,:material-check-bold:," "," "," "
Different package with same name," "," "," "," "
Containing another struct from same package," "," "," "," "
Containing another struct from different package," "," "," "," "
Containing an anonymous struct," "," "," "," "
Containing another struct from same package,:material-check-bold:," "," "," "
Containing another struct from different package,:material-check-bold:," "," "," "
Containing an anonymous struct,:material-check-bold:," "," "," "
1 Scenario Windows Mac Linux WSL
2 Same package :material-check-bold:
3 Different package :material-check-bold:
4 Different package with same name
5 Containing another struct from same package :material-check-bold:
6 Containing another struct from different package :material-check-bold:
7 Containing an anonymous struct :material-check-bold:
+3 -3
View File
@@ -1,10 +1,10 @@
Scenario,Windows,Mac,Linux,WSL
Class model for struct in same package,:material-check-bold:," "," "," "
Class model for struct in different package,:material-check-bold:," "," "," "
Interface model for struct in same package," "," "," "," "
Interface model for struct in different package," "," "," "," "
Interface model for struct in same package,:material-check-bold:," "," "," "
Interface model for struct in different package,:material-check-bold:," "," "," "
Enum in same package,:material-check-bold:," "," "," "
Enum in different package,:material-check-bold:," "," "," "
Interface using enum in same package,:material-check-bold:," "," "," "
Interface using enum in different package," "," "," "," "
Interface using enum in different package,:material-check-bold:," "," "," "
1 Scenario Windows Mac Linux WSL
2 Class model for struct in same package :material-check-bold:
3 Class model for struct in different package :material-check-bold:
4 Interface model for struct in same package :material-check-bold:
5 Interface model for struct in different package :material-check-bold:
6 Enum in same package :material-check-bold:
7 Enum in different package :material-check-bold:
8 Interface using enum in same package :material-check-bold:
9 Interface using enum in different package :material-check-bold:
10
+1
View File
@@ -7,6 +7,7 @@ toolchain go1.21.4
require (
github.com/atterpac/refresh v0.2.4
github.com/bep/debounce v1.2.1
github.com/davecgh/go-spew v1.1.1
github.com/ebitengine/purego v0.4.0-alpha.4
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ole/go-ole v1.2.6
+98 -44
View File
@@ -16,23 +16,26 @@ const header = `// @ts-check
`
const bindingTemplate = `
/**
* {{structName}}.{{methodName}}
*Comments
* @param names {string}
* @returns {Promise<string>}
**/
/**Comments
* @function {{methodName}}* @param names {string}
* @returns {Promise<string>}
**/
`
const callByID = ` {{methodName}}: function({{inputs}}) { return wails.CallByID({{ID}}, ...Array.prototype.slice.call(arguments, 0)); },
const callByID = `export function {{methodName}}({{inputs}}) {
return wails.CallByID({{ID}}, ...Array.prototype.slice.call(arguments, 0));
}
`
const callByName = ` {{methodName}}: function({{inputs}}) { return wails.CallByName("{{Name}}", ...Array.prototype.slice.call(arguments, 0)); },
const callByName = `export function {{methodName}}({{inputs}}) {
return wails.CallByName("{{Name}}", ...Array.prototype.slice.call(arguments, 0));
}
`
const enumTemplate = `
export enum {{.EnumName}} {
{{.EnumValues}}
}
export enum {{.EnumName}} {
{{.EnumValues}}
}
`
var reservedWords = []string{
@@ -112,8 +115,13 @@ func sanitiseJSVarName(name string) string {
return name
}
func GenerateBinding(structName string, method *BoundMethod, useIDs bool) (string, []string, []string) {
var namespacedStructs []string
type ExternalStruct struct {
Package string
Name string
}
func GenerateBinding(thisStructName string, method *BoundMethod, useIDs bool) (string, []string, map[packagePath]map[string]*ExternalStruct) {
var externalStructs = make(map[packagePath]map[string]*ExternalStruct)
var models []string
template := bindingTemplate
if useIDs {
@@ -121,7 +129,7 @@ func GenerateBinding(structName string, method *BoundMethod, useIDs bool) (strin
} else {
template += callByName
}
result := strings.ReplaceAll(template, "{{structName}}", structName)
result := strings.ReplaceAll(template, "{{structName}}", thisStructName)
result = strings.ReplaceAll(result, "{{methodName}}", method.Name)
result = strings.ReplaceAll(result, "{{ID}}", fmt.Sprintf("%v", method.ID))
@@ -129,10 +137,10 @@ func GenerateBinding(structName string, method *BoundMethod, useIDs bool) (strin
parts := strings.Split(method.Package, "/")
packageName := parts[len(parts)-1]
result = strings.ReplaceAll(result, "{{Name}}", fmt.Sprintf("%v.%v.%v", packageName, structName, method.Name))
result = strings.ReplaceAll(result, "{{Name}}", fmt.Sprintf("%v.%v.%v", packageName, thisStructName, method.Name))
comments := strings.TrimSpace(method.DocComment)
if comments != "" {
comments = " " + comments
comments = "\n * " + comments
}
result = strings.ReplaceAll(result, "Comments", comments)
var params string
@@ -143,16 +151,22 @@ func GenerateBinding(structName string, method *BoundMethod, useIDs bool) (strin
models = append(models, pkgName)
}
if input.Type.IsStruct || input.Type.IsEnum {
nsStruct := input.NamespacedStructType()
namespacedStructs = append(namespacedStructs, nsStruct)
if _, ok := externalStructs[input.Type.Package]; !ok {
externalStructs[input.Type.Package] = make(map[string]*ExternalStruct)
}
externalStructs[input.Type.Package][input.Type.Name] = &ExternalStruct{
Package: input.Type.Package,
Name: input.Type.Name,
}
}
params += " * @param " + inputName + " {" + input.JSType() + "}\n"
inputType := input.JSType(packageName)
params += "\n * @param " + inputName + " {" + inputType + "}"
}
params = strings.TrimSuffix(params, "\n")
if len(params) == 0 {
params = " *"
}
//if len(params) > 0 {
// params = "\n" + params
//}
result = strings.ReplaceAll(result, "* @param names {string}", params)
var inputs string
for _, input := range method.Inputs {
@@ -181,13 +195,19 @@ func GenerateBinding(structName string, method *BoundMethod, useIDs bool) (strin
if pkgName != "" {
models = append(models, pkgName)
}
jsType := output.JSType()
jsType := output.JSType(packageName)
if jsType == "error" {
jsType = "void"
}
if output.Type.IsStruct {
namespacedStructs = append(namespacedStructs, output.NamespacedStructType())
jsType = output.NamespacedStructVariable()
if _, ok := externalStructs[output.Type.Package]; !ok {
externalStructs[output.Type.Package] = make(map[string]*ExternalStruct)
}
externalStructs[output.Type.Package][output.Type.Name] = &ExternalStruct{
Package: output.Type.Package,
Name: output.Type.Name,
}
jsType = output.NamespacedStructVariable(output.Type.Package)
}
returns += jsType + ", "
}
@@ -196,7 +216,7 @@ func GenerateBinding(structName string, method *BoundMethod, useIDs bool) (strin
}
result = strings.ReplaceAll(result, " * @returns {Promise<string>}", returns)
return result, lo.Uniq(models), lo.Uniq(namespacedStructs)
return result, lo.Uniq(models), externalStructs
}
func getPackageName(input *Parameter) string {
@@ -240,47 +260,81 @@ func normalisePackageNames(packageNames []string) map[string]string {
return result
}
func GenerateBindings(bindings map[string]map[string][]*BoundMethod, useIDs bool) map[string]string {
func (p *Project) GenerateBindings(bindings map[string]map[string][]*BoundMethod, useIDs bool) map[string]map[string]string {
var result = make(map[string]string)
var result = make(map[string]map[string]string)
var normalisedPackageNames = normalisePackageNames(lo.Keys(bindings))
// sort the bindings keys
packageNames := lo.Keys(bindings)
sort.Strings(packageNames)
for _, packageName := range packageNames {
var allModels []string
var allNamespacedStructs []string
packageBindings := bindings[packageName]
structNames := lo.Keys(packageBindings)
relativePackageDir := p.RelativePackageDir(packageName)
_ = relativePackageDir
sort.Strings(structNames)
for _, structName := range structNames {
result[normalisedPackageNames[packageName]] += "export const " + structName + " = {\n"
if _, ok := result[relativePackageDir]; !ok {
result[relativePackageDir] = make(map[string]string)
}
methods := packageBindings[structName]
sort.Slice(methods, func(i, j int) bool {
return methods[i].Name < methods[j].Name
})
var allNamespacedStructs map[packagePath]map[string]*ExternalStruct
var namespacedStructs map[packagePath]map[string]*ExternalStruct
var thisBinding string
var models []string
for _, method := range methods {
thisBinding, models, namespacedStructs := GenerateBinding(structName, method, useIDs)
allNamespacedStructs = append(allNamespacedStructs, namespacedStructs...)
thisBinding, models, namespacedStructs = GenerateBinding(structName, method, useIDs)
// Merge the namespaced structs
allNamespacedStructs = mergeNamespacedStructs(allNamespacedStructs, namespacedStructs)
allModels = append(allModels, models...)
result[normalisedPackageNames[packageName]] += thisBinding
result[relativePackageDir][structName] += thisBinding
}
result[normalisedPackageNames[packageName]] += "};\n\n"
}
if len(allNamespacedStructs) > 0 {
typedefs := "/**\n"
for _, namespacedStruct := range lo.Uniq(allNamespacedStructs) {
typedefs += " * @typedef {import('./models')." + namespacedStruct + "} " + strings.ReplaceAll(namespacedStruct, ".", "") + "\n"
if len(allNamespacedStructs) > 0 {
thisPkg := p.packageCache[packageName]
typedefs := "/**\n"
for externalPackageName, namespacedStruct := range allNamespacedStructs {
pkgInfo := p.packageCache[externalPackageName]
relativePackageDir := p.RelativeBindingsDir(thisPkg, pkgInfo)
namePrefix := ""
if pkgInfo.Name != "" && pkgInfo.Path != thisPkg.Path {
namePrefix = pkgInfo.Name
}
// Get keys from namespacedStruct and iterate over them in sorted order
namespacedStructNames := lo.Keys(namespacedStruct)
sort.Strings(namespacedStructNames)
for _, thisStructName := range namespacedStructNames {
structInfo := namespacedStruct[thisStructName]
typedefs += " * @typedef {import('" + relativePackageDir + "/models')." + thisStructName + "} " + namePrefix + structInfo.Name + "\n"
}
}
typedefs += " */\n"
result[relativePackageDir][structName] = typedefs + result[relativePackageDir][structName]
}
typedefs += " */\n\n"
result[normalisedPackageNames[packageName]] = typedefs + result[normalisedPackageNames[packageName]]
result[relativePackageDir][structName] = header + result[relativePackageDir][structName]
}
result[normalisedPackageNames[packageName]] = header + result[normalisedPackageNames[packageName]]
}
return result
}
func mergeNamespacedStructs(structs map[packagePath]map[string]*ExternalStruct, structs2 map[packagePath]map[string]*ExternalStruct) map[packagePath]map[string]*ExternalStruct {
if structs == nil {
structs = make(map[packagePath]map[string]*ExternalStruct)
}
for pkg, pkgStructs := range structs2 {
if _, ok := structs[pkg]; !ok {
structs[pkg] = make(map[string]*ExternalStruct)
}
for name, structInfo := range pkgStructs {
structs[pkg][name] = structInfo
}
}
return structs
}
+287 -80
View File
@@ -2,11 +2,11 @@ package parser
import (
"embed"
"github.com/google/go-cmp/cmp"
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
)
//go:embed testdata
@@ -25,135 +25,342 @@ func TestGenerateBindings(t *testing.T) {
tests := []struct {
dir string
want map[string]string
want map[string]map[string]string
useIDs bool
}{
{
"testdata/enum",
map[string]string{
"main": getFile("testdata/enum/bindings_main.js"),
dir: "testdata/enum",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/enum/frontend/bindings/main/GreetService.js"),
},
},
true,
useIDs: true,
},
{
"testdata/enum",
map[string]string{
"main": getFile("testdata/enum/bindings_main.name.js"),
dir: "testdata/enum",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/enum/frontend/bindings/main/GreetService.name.js"),
},
},
false,
},
// TODO: Fix this test to pull in enum
{
"testdata/enum_from_imported_package",
map[string]string{
"main": getFile("testdata/enum_from_imported_package/bindings_main.js"),
},
true,
},
{
"testdata/function_single",
map[string]string{
"main": getFile("testdata/function_single/bindings_main.js"),
dir: "testdata/enum_from_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/enum_from_imported_package/frontend/bindings/main/GreetService.name.js"),
},
},
true,
},
{
"testdata/function_from_imported_package",
map[string]string{
"main": getFile("testdata/function_from_imported_package/bindings_main.js"),
"services": getFile("testdata/function_from_imported_package/bindings_services.js"),
dir: "testdata/enum_from_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/enum_from_imported_package/frontend/bindings/main/GreetService.js"),
},
},
true,
useIDs: true,
},
{
"testdata/variable_single",
map[string]string{
"main": getFile("testdata/variable_single/bindings_main.js"),
dir: "testdata/function_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_single/frontend/bindings/main/GreetService.js"),
},
},
true,
useIDs: true,
},
{
"testdata/variable_single_from_function",
map[string]string{
"main": getFile("testdata/variable_single_from_function/bindings_main.js"),
dir: "testdata/function_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_single/frontend/bindings/main/GreetService.name.js"),
},
},
true,
useIDs: false,
},
{
"testdata/variable_single_from_other_function",
map[string]string{
"main": getFile("testdata/variable_single_from_other_function/bindings_main.js"),
"services": getFile("testdata/variable_single_from_other_function/bindings_services.js"),
dir: "testdata/function_from_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_from_imported_package/frontend/bindings/main/GreetService.name.js"),
},
"services": {
"OtherService": getFile("testdata/function_from_imported_package/frontend/bindings/services/OtherService.name.js"),
},
},
true,
useIDs: false,
},
{
"testdata/struct_literal_single",
map[string]string{
"main": getFile("testdata/struct_literal_single/bindings_main.js"),
dir: "testdata/function_from_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_from_imported_package/frontend/bindings/main/GreetService.js"),
},
"services": {
"OtherService": getFile("testdata/function_from_imported_package/frontend/bindings/services/OtherService.js"),
},
},
true,
useIDs: true,
},
{
"testdata/struct_literal_multiple",
map[string]string{
"main": getFile("testdata/struct_literal_multiple/bindings_main.js"),
dir: "testdata/function_from_nested_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_from_nested_imported_package/frontend/bindings/main/GreetService.name.js"),
},
"services/other": {
"OtherService": getFile("testdata/function_from_nested_imported_package/frontend/bindings/services/other/OtherService.name.js"),
},
},
true,
useIDs: false,
},
{
"testdata/struct_literal_multiple_other",
map[string]string{
"main": getFile("testdata/struct_literal_multiple_other/bindings_main.js"),
"services": getFile("testdata/struct_literal_multiple_other/bindings_services.js"),
dir: "testdata/function_from_nested_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_from_nested_imported_package/frontend/bindings/main/GreetService.js"),
},
"services/other": {
"OtherService": getFile("testdata/function_from_nested_imported_package/frontend/bindings/services/other/OtherService.js"),
},
},
true,
useIDs: true,
},
{
"testdata/struct_literal_multiple_files",
map[string]string{
"main": getFile("testdata/struct_literal_multiple_files/bindings_main.js"),
dir: "testdata/struct_literal_multiple",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_multiple/frontend/bindings/main/GreetService.js"),
"OtherService": getFile("testdata/struct_literal_multiple/frontend/bindings/main/OtherService.js"),
},
},
true,
useIDs: true,
},
{
dir: "testdata/struct_literal_multiple",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_multiple/frontend/bindings/main/GreetService.name.js"),
"OtherService": getFile("testdata/struct_literal_multiple/frontend/bindings/main/OtherService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/function_from_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_from_imported_package/frontend/bindings/main/GreetService.js"),
},
"services": {
"OtherService": getFile("testdata/function_from_imported_package/frontend/bindings/services/OtherService.js"),
},
},
useIDs: true,
},
{
dir: "testdata/function_from_imported_package",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/function_from_imported_package/frontend/bindings/main/GreetService.name.js"),
},
"services": {
"OtherService": getFile("testdata/function_from_imported_package/frontend/bindings/services/OtherService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/variable_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/variable_single/frontend/bindings/main/GreetService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/variable_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/variable_single/frontend/bindings/main/GreetService.js"),
},
},
useIDs: true,
},
{
dir: "testdata/variable_single_from_function",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/variable_single_from_function/frontend/bindings/main/GreetService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/variable_single_from_function",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/variable_single_from_function/frontend/bindings/main/GreetService.js"),
},
},
useIDs: true,
},
{
dir: "testdata/variable_single_from_other_function",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/variable_single_from_other_function/frontend/bindings/main/GreetService.name.js"),
},
"services": {
"OtherService": getFile("testdata/variable_single_from_other_function/frontend/bindings/services/OtherService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/variable_single_from_other_function",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/variable_single_from_other_function/frontend/bindings/main/GreetService.js"),
},
"services": {
"OtherService": getFile("testdata/variable_single_from_other_function/frontend/bindings/services/OtherService.js"),
},
},
useIDs: true,
},
{
dir: "testdata/struct_literal_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_single/frontend/bindings/main/GreetService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/struct_literal_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_single/frontend/bindings/main/GreetService.js"),
},
},
useIDs: true,
},
{
dir: "testdata/struct_literal_multiple_other",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_multiple_other/frontend/bindings/main/GreetService.name.js"),
},
"services": {
"OtherService": getFile("testdata/struct_literal_multiple_other/frontend/bindings/services/OtherService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/struct_literal_multiple_other",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_multiple_other/frontend/bindings/main/GreetService.js"),
},
"services": {
"OtherService": getFile("testdata/struct_literal_multiple_other/frontend/bindings/services/OtherService.js"),
},
},
useIDs: true,
},
{
dir: "testdata/struct_literal_non_pointer_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_non_pointer_single/frontend/bindings/main/GreetService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/struct_literal_non_pointer_single",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_non_pointer_single/frontend/bindings/main/GreetService.js"),
},
},
useIDs: true,
},
{
dir: "testdata/struct_literal_multiple_files",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_multiple_files/frontend/bindings/main/GreetService.name.js"),
"OtherService": getFile("testdata/struct_literal_multiple_files/frontend/bindings/main/OtherService.name.js"),
},
},
useIDs: false,
},
{
dir: "testdata/struct_literal_multiple_files",
want: map[string]map[string]string{
"main": {
"GreetService": getFile("testdata/struct_literal_multiple_files/frontend/bindings/main/GreetService.js"),
"OtherService": getFile("testdata/struct_literal_multiple_files/frontend/bindings/main/OtherService.js"),
},
},
useIDs: true,
},
}
for _, tt := range tests {
t.Run(tt.dir, func(t *testing.T) {
// Run parser on directory
project, err := ParseProject(tt.dir)
absDir, err := filepath.Abs(tt.dir)
if err != nil {
t.Errorf("filepath.Abs() error = %v", err)
return
}
project, err := ParseProject(absDir)
if err != nil {
t.Errorf("ParseProject() error = %v", err)
return
}
project.outputDirectory = "frontend/bindings"
// Generate Bindings
got := GenerateBindings(project.BoundMethods, tt.useIDs)
got := project.GenerateBindings(project.BoundMethods, tt.useIDs)
for name, binding := range got {
// check if the binding is in the expected bindings
expected, ok := tt.want[name]
if !ok {
err = os.WriteFile(tt.dir+"/bindings_"+name+".got.js", []byte(binding), 0644)
if err != nil {
t.Errorf("os.WriteFile() error = %v", err)
for dirName, structDetails := range got {
// iterate the struct names in structDetails
for name, binding := range structDetails {
expected, ok := tt.want[dirName][name]
if !ok {
outFile := filepath.Join(tt.dir, project.outputDirectory, dirName, name+".got.js")
err = os.WriteFile(outFile, []byte(binding), 0644)
if err != nil {
t.Errorf("os.WriteFile() error = %v", err)
return
}
t.Errorf("GenerateBindings() unexpected binding = %v", name)
return
}
t.Errorf("GenerateBindings() unexpected binding = %v", name)
return
}
// compare the binding
// compare the binding
// convert all line endings to \n
binding = convertLineEndings(binding)
expected = convertLineEndings(expected)
// convert all line endings to \n
binding = convertLineEndings(binding)
expected = convertLineEndings(expected)
if diff := cmp.Diff(expected, binding); diff != "" {
err = os.WriteFile(tt.dir+"/bindings_"+name+".got.js", []byte(binding), 0644)
if err != nil {
t.Errorf("os.WriteFile() error = %v", err)
return
if diff := cmp.Diff(expected, binding); diff != "" {
outFile := filepath.Join(tt.dir, project.outputDirectory, dirName, name+".got.js")
err = os.WriteFile(outFile, []byte(binding), 0644)
if err != nil {
t.Errorf("os.WriteFile() error = %v", err)
return
}
t.Fatalf("GenerateBindings() mismatch (-want +got):\n%s", diff)
}
t.Fatalf("GenerateBindings() mismatch (-want +got):\n%s", diff)
}
}
})
+20 -10
View File
@@ -17,9 +17,10 @@ type ModelDefinitions struct {
Package string
Models map[string]*StructDef
Enums map[string]*TypeDef
Imports []*ImportDef
}
func GenerateModel(wr io.Writer, def *ModelDefinitions, options *flags.GenerateBindingsOptions) error {
func (p *Project) GenerateModel(wr io.Writer, def *ModelDefinitions, options *flags.GenerateBindingsOptions) error {
templateName := "model.js.tmpl"
if options.TS {
templateName = "model.ts.tmpl"
@@ -57,13 +58,15 @@ func pkgAlias(fullPkg string) string {
return pkgParts[len(pkgParts)-1]
}
func GenerateModels(models map[packagePath]map[structName]*StructDef, enums map[packagePath]map[string]*TypeDef, options *flags.GenerateBindingsOptions) (string, error) {
if models == nil && enums == nil {
return "", nil
}
type Model struct {
Package string
}
var buffer bytes.Buffer
buffer.WriteString(modelsHeader)
func (p *Project) GenerateModels(models map[packagePath]map[structName]*StructDef, enums map[packagePath]map[string]*TypeDef, options *flags.GenerateBindingsOptions) (map[packagePath]string, error) {
if models == nil && enums == nil {
return nil, nil
}
var result = make(map[packagePath]string)
// 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
@@ -80,14 +83,21 @@ func GenerateModels(models map[packagePath]map[structName]*StructDef, enums map[
})
for _, pkg := range keys {
err := GenerateModel(&buffer, &ModelDefinitions{
var buffer bytes.Buffer
buffer.WriteString(modelsHeader)
err := p.GenerateModel(&buffer, &ModelDefinitions{
Imports: p.calculateImports(pkg, models[pkg]),
Package: pkgAlias(pkg),
Models: models[pkg],
Enums: enums[pkg],
}, options)
if err != nil {
return "", err
return nil, err
}
// Get the relative package path
relativePackageDir := p.RelativePackageDir(pkg)
result[relativePackageDir] = buffer.String()
}
return buffer.String(), nil
return result, nil
}
+205 -104
View File
@@ -14,109 +14,199 @@ func TestGenerateModels(t *testing.T) {
tests := []struct {
name string
dir string
want string
want map[string]string
useInterface bool
useTypescript bool
}{
// enum
{
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.js"),
},
{
name: "variable single",
dir: "testdata/variable_single",
useTypescript: true,
},
{
name: "variable single from function",
dir: "testdata/variable_single_from_function",
useTypescript: true,
},
{
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,
},
{
name: "struct literal single",
dir: "testdata/struct_literal_single",
want: getFile("testdata/struct_literal_single/models.ts"),
useTypescript: true,
},
{
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.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,
},
{
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)",
name: "enum - Typescript",
dir: "testdata/enum",
want: getFile("testdata/enum/models.js"),
},
{
name: "enum from imported package",
dir: "testdata/enum_from_imported_package",
want: getFile("testdata/enum_from_imported_package/models.ts"),
want: map[string]string{
"main": getFile("testdata/enum/frontend/bindings/main/models.ts"),
},
useTypescript: true,
},
{
name: "enum from imported package",
dir: "testdata/enum_from_imported_package",
want: getFile("testdata/enum_from_imported_package/models.js"),
name: "enum - Javascript",
dir: "testdata/enum",
want: map[string]string{
"main": getFile("testdata/enum/frontend/bindings/main/models.js"),
},
useTypescript: false,
},
{
name: "enum interface",
dir: "testdata/enum-interface",
want: getFile("testdata/enum-interface/models.ts"),
useInterface: true,
name: "enum - Typescript interfaces",
dir: "testdata/enum",
want: map[string]string{
"main": getFile("testdata/enum/frontend/bindings/main/models.interfaces.ts"),
},
useTypescript: true,
useInterface: true,
},
// function from imported package
{
name: "function from imported package - Typescript",
dir: "testdata/function_from_imported_package",
want: map[string]string{
"main": getFile("testdata/function_from_imported_package/frontend/bindings/main/models.ts"),
"services": getFile("testdata/function_from_imported_package/frontend/bindings/services/models.ts"),
},
useTypescript: true,
},
{
name: "function from imported package - Typescript interfaces",
dir: "testdata/function_from_imported_package",
want: map[string]string{
"main": getFile("testdata/function_from_imported_package/frontend/bindings/main/models.interfaces.ts"),
"services": getFile("testdata/function_from_imported_package/frontend/bindings/services/models.interfaces.ts"),
},
useTypescript: true,
useInterface: true,
},
{
name: "function from imported package - Javascript",
dir: "testdata/function_from_imported_package",
want: map[string]string{
"main": getFile("testdata/function_from_imported_package/frontend/bindings/main/models.js"),
"services": getFile("testdata/function_from_imported_package/frontend/bindings/services/models.js"),
},
useTypescript: false,
},
// variable single from other function
{
name: "variable single from other function - Typescript",
dir: "testdata/variable_single_from_other_function",
want: map[string]string{
"main": getFile("testdata/variable_single_from_other_function/frontend/bindings/main/models.ts"),
"services": getFile("testdata/variable_single_from_other_function/frontend/bindings/services/models.ts"),
},
useTypescript: true,
},
{
name: "variable single from other function - Typescript interfaces",
dir: "testdata/variable_single_from_other_function",
want: map[string]string{
"main": getFile("testdata/variable_single_from_other_function/frontend/bindings/main/models.interfaces.ts"),
"services": getFile("testdata/variable_single_from_other_function/frontend/bindings/services/models.interfaces.ts"),
},
useTypescript: true,
useInterface: true,
},
{
name: "variable single from other function - Javascript",
dir: "testdata/variable_single_from_other_function",
want: map[string]string{
"main": getFile("testdata/variable_single_from_other_function/frontend/bindings/main/models.js"),
"services": getFile("testdata/variable_single_from_other_function/frontend/bindings/services/models.js"),
},
useTypescript: false,
},
// struct literal single
{
name: "struct literal single - Typescript",
dir: "testdata/struct_literal_single",
want: map[string]string{
"main": getFile("testdata/struct_literal_single/frontend/bindings/main/models.ts"),
},
useTypescript: true,
},
{
name: "struct literal single - Typescript interfaces",
dir: "testdata/struct_literal_single",
want: map[string]string{
"main": getFile("testdata/struct_literal_single/frontend/bindings/main/models.interfaces.ts"),
},
useTypescript: true,
useInterface: true,
},
{
name: "struct literal single - Javascript",
dir: "testdata/struct_literal_single",
want: map[string]string{
"main": getFile("testdata/struct_literal_single/frontend/bindings/main/models.js"),
},
useTypescript: false,
},
// struct literal multiple other
{
name: "struct literal multiple other - Typescript",
dir: "testdata/struct_literal_multiple_other",
want: map[string]string{
"main": getFile("testdata/struct_literal_multiple_other/frontend/bindings/main/models.ts"),
"services": getFile("testdata/struct_literal_multiple_other/frontend/bindings/services/models.ts"),
},
useTypescript: true,
},
{
name: "struct literal multiple other - Typescript interfaces",
dir: "testdata/struct_literal_multiple_other",
want: map[string]string{
"main": getFile("testdata/struct_literal_multiple_other/frontend/bindings/main/models.interfaces.ts"),
"services": getFile("testdata/struct_literal_multiple_other/frontend/bindings/services/models.interfaces.ts"),
},
useTypescript: true,
useInterface: true,
},
{
name: "struct literal multiple other - Javascript",
dir: "testdata/struct_literal_multiple_other",
want: map[string]string{
"main": getFile("testdata/struct_literal_multiple_other/frontend/bindings/main/models.js"),
"services": getFile("testdata/struct_literal_multiple_other/frontend/bindings/services/models.js"),
},
useTypescript: false,
},
// struct literal non pointer single
{
name: "struct literal non pointer single - Typescript",
dir: "testdata/struct_literal_non_pointer_single",
want: map[string]string{
"main": getFile("testdata/struct_literal_non_pointer_single/frontend/bindings/main/models.ts"),
},
useTypescript: true,
},
{
name: "struct literal non pointer single - Typescript interfaces",
dir: "testdata/struct_literal_non_pointer_single",
want: map[string]string{
"main": getFile("testdata/struct_literal_non_pointer_single/frontend/bindings/main/models.interfaces.ts"),
},
useTypescript: true,
useInterface: true,
},
{
name: "struct literal non pointer single - Javascript",
dir: "testdata/struct_literal_non_pointer_single",
want: map[string]string{
"main": getFile("testdata/struct_literal_non_pointer_single/frontend/bindings/main/models.js"),
},
useTypescript: false,
},
// enum from imported package
{
name: "enum from imported package - Typescript",
dir: "testdata/enum_from_imported_package",
want: map[string]string{
"services": getFile("testdata/enum_from_imported_package/frontend/bindings/services/models.ts"),
},
useTypescript: true,
},
{
name: "enum from imported package - Typescript interfaces",
dir: "testdata/enum_from_imported_package",
want: map[string]string{
"services": getFile("testdata/enum_from_imported_package/frontend/bindings/services/models.interfaces.ts"),
},
useTypescript: true,
},
{
name: "enum from imported package - Javascript",
dir: "testdata/enum_from_imported_package",
want: map[string]string{
"services": getFile("testdata/enum_from_imported_package/frontend/bindings/services/models.js"),
},
useTypescript: false,
},
}
for _, tt := range tests {
@@ -127,28 +217,39 @@ func TestGenerateModels(t *testing.T) {
t.Fatalf("ParseProject() error = %v", err)
}
project.outputDirectory = "frontend/bindings"
// Generate Models
got, err := GenerateModels(project.Models, project.Types, &flags.GenerateBindingsOptions{
allModels, err := project.GenerateModels(project.Models, project.Types, &flags.GenerateBindingsOptions{
UseInterfaces: tt.useInterface,
TS: tt.useTypescript,
})
if err != nil {
t.Fatalf("GenerateModels() error = %v", err)
}
// convert all line endings to \n
got = convertLineEndings(got)
want := convertLineEndings(tt.want)
if diff := cmp.Diff(want, got); diff != "" {
gotFilename := "models.got.js"
if tt.useTypescript {
gotFilename = "models.got.ts"
for pkgDir, got := range allModels {
// convert all line endings to \n
got = convertLineEndings(got)
want, ok := tt.want[pkgDir]
if !ok {
t.Fatalf("GenerateModels() missing package: %s", pkgDir)
}
err = os.WriteFile(filepath.Join(tt.dir, gotFilename), []byte(got), 0644)
if err != nil {
t.Errorf("os.WriteFile() error = %v", err)
return
want = convertLineEndings(want)
if diff := cmp.Diff(want, got); diff != "" {
gotFilename := "models.got.js"
if tt.useTypescript {
gotFilename = "models.got.ts"
}
// Get relative package path
//relativeBindingsDir := project.RelativeBindingsDir(project.packageCache[pkgDir])
err = os.WriteFile(filepath.Join(tt.dir, project.outputDirectory, pkgDir, gotFilename), []byte(got), 0644)
if err != nil {
t.Errorf("os.WriteFile() error = %v", err)
return
}
t.Fatalf("GenerateModels() mismatch (-want +got):\n%s", diff)
}
t.Fatalf("GenerateModels() mismatch (-want +got):\n%s", diff)
}
})
}
+125 -32
View File
@@ -23,9 +23,9 @@ type packagePath = string
type structName = string
type StructDef struct {
Name string
DocComment string
Fields []*Field
Name string
DocComments []string
Fields []*Field
}
func (s *StructDef) DefaultValueList() string {
@@ -66,24 +66,24 @@ type Parameter struct {
Type *ParameterType
}
func (p *Parameter) NamespacedStructType() string {
func (p *Parameter) NamespacedStructType(pkgName string) string {
var typeName string
if p.Type.Package != "" {
if p.Type.Package != "" && p.Type.Package != pkgName {
parts := strings.Split(p.Type.Package, "/")
typeName = parts[len(parts)-1] + "."
}
return typeName + p.Type.Name
}
func (p *Parameter) NamespacedStructVariable() string {
func (p *Parameter) NamespacedStructVariable(pkgName string) string {
var typeName string
if p.Type.Package != "" {
if p.Type.Package != "" && p.Type.Package != pkgName {
parts := strings.Split(p.Type.Package, "/")
typeName = parts[len(parts)-1]
}
return typeName + p.Type.Name
}
func (p *Parameter) JSType() string {
func (p *Parameter) JSType(pkgName string) string {
// Convert type to javascript equivalent type
var typeName string
switch p.Type.Name {
@@ -99,7 +99,7 @@ func (p *Parameter) JSType() string {
// if the type is a struct, we need to add the package name
if p.Type.IsStruct || p.Type.IsEnum {
typeName = p.NamespacedStructType()
typeName = p.NamespacedStructType(pkgName)
typeName = strings.ReplaceAll(typeName, ".", "")
}
@@ -118,6 +118,7 @@ func (p *Parameter) JSType() string {
type BoundMethod struct {
Package string
PackageDir string
Name string
DocComment string
Inputs []*Parameter
@@ -131,8 +132,9 @@ func (m BoundMethod) IDAsString() string {
}
type Field struct {
Name string
Type *ParameterType
Name string
Type *ParameterType
Project *Project
}
func (f *Field) JSName() string {
@@ -195,17 +197,24 @@ func (f *Field) JSDocType(pkg string) string {
jsType = f.Type.Name
}
// If we are the same package, just return the type
externalPkgInfo := f.Project.packageCache[f.Type.Package]
if externalPkgInfo.Name == pkg {
return jsType
}
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.Package == "" || !isExternalStruct {
if f.Type.IsStruct || f.Type.IsEnum {
result = fmt.Sprintf("%s.%s", pkg, jsType)
// get the relative package directory
result = fmt.Sprintf("%s%s", externalPkgInfo.Name, f.Type.Name)
} else {
result = jsType
}
} else {
parts := strings.Split(f.Type.Package, "/")
result += fmt.Sprintf("%s.%s", parts[len(parts)-1], jsType)
// get the relative package directory
result = fmt.Sprintf("%s%s", externalPkgInfo.Name, f.Type.Name)
}
if !ast.IsExported(f.Name) {
@@ -230,9 +239,9 @@ func (f *Field) DefaultValue() string {
}
type ConstDef struct {
Name string
DocComment string
Value string
Name string
DocComments []string
Value string
}
type TypeDef struct {
@@ -258,6 +267,7 @@ type ParsedPackage struct {
type Project struct {
packageCache map[string]*ParsedPackage
outputDirectory string
Path string
BoundMethods map[packagePath]map[structName][]*BoundMethod
Models map[packagePath]map[structName]*StructDef
@@ -320,13 +330,21 @@ func GenerateBindingsAndModels(options *flags.GenerateBindingsOptions) error {
return err
}
p.Stats.NumMethods = len(p.BoundMethods)
generatedMethods := GenerateBindings(p.BoundMethods, options.UseIDs)
for pkg, text := range generatedMethods {
// Write the file
err = os.WriteFile(filepath.Join(options.OutputDirectory, pkg+".js"), []byte(text), 0644)
if err != nil {
p.outputDirectory = options.OutputDirectory
generatedMethods := p.GenerateBindings(p.BoundMethods, options.UseIDs)
for pkg, structs := range generatedMethods {
// Write the directory
err = os.MkdirAll(filepath.Join(options.OutputDirectory, pkg), 0755)
if err != nil && !os.IsExist(err) {
return err
}
// Write the files
for structName, text := range structs {
err = os.WriteFile(filepath.Join(options.OutputDirectory, pkg, structName+".js"), []byte(text), 0644)
if err != nil {
return err
}
}
}
p.Stats.NumModels = len(p.Models)
@@ -334,11 +352,17 @@ func GenerateBindingsAndModels(options *flags.GenerateBindingsOptions) error {
// Generate Models
if len(p.Models) > 0 {
generatedModels, err := GenerateModels(p.Models, p.Types, options)
generatedModels, err := p.GenerateModels(p.Models, p.Types, options)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(options.OutputDirectory, options.ModelsFilename), []byte(generatedModels), 0644)
for pkg, text := range generatedModels {
// Get directory for package
pkgInfo := p.packageCache[pkg]
relativePackageDir := p.RelativeBindingsDir(pkgInfo, pkgInfo)
// Write the directory
err = os.WriteFile(filepath.Join(options.OutputDirectory, relativePackageDir, options.ModelsFilename), []byte(text), 0644)
}
if err != nil {
return err
}
@@ -602,6 +626,7 @@ func (p *Project) parseBoundStructMethods(name string, pkg *ParsedPackage) error
method := &BoundMethod{
Package: pkg.Path,
PackageDir: pkg.Dir,
ID: id,
Name: funcDecl.Name.Name,
DocComment: strings.TrimSpace(funcDecl.Doc.Text()),
@@ -689,7 +714,8 @@ func (p *Project) parseParameterType(field *ast.Field, pkg *ParsedPackage) *Para
result.Name = p.anonymousStructID()
// Create a new struct definition
result := &StructDef{
Name: result.Name,
Name: result.Name,
DocComments: CommentGroupToText(field.Doc),
}
pkg.StructCache[result.Name] = result
// Parse the fields
@@ -750,8 +776,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,
//TODO DocComment: CommentGroupToText(typeDecl.Doc),
Name: name,
DocComments: CommentGroupToText(typeDecl.Doc),
}
pkg.StructCache[name] = result
result.Fields = p.parseStructFields(structType, pkg)
@@ -774,12 +800,14 @@ func (p *Project) parseStructFields(structType *ast.StructType, pkg *ParsedPacka
if len(field.Names) > 0 {
for _, name := range field.Names {
theseFields = append(theseFields, &Field{
Name: name.Name,
Project: p,
Name: name.Name,
})
}
} else {
theseFields = append(theseFields, &Field{
Name: "",
Project: p,
Name: "",
})
}
// loop over fields
@@ -1079,7 +1107,7 @@ func (p *Project) parseConstDeclaration(decl *ast.GenDecl, pkg *ParsedPackage) {
}
if valueSpec.Doc != nil {
constDecl.DocComment = strings.TrimSpace(valueSpec.Doc.Text())
constDecl.DocComments = CommentGroupToText(valueSpec.Doc)
}
typeDecl.Consts = append(typeDecl.Consts, constDecl)
}
@@ -1087,7 +1115,22 @@ func (p *Project) parseConstDeclaration(decl *ast.GenDecl, pkg *ParsedPackage) {
}
func (p *Project) RelativePackageDir(path string) string {
return strings.TrimPrefix(path, p.Path)
// Get the package details
pkgInfo, ok := p.packageCache[path]
if !ok {
panic("package not found: " + path)
}
result := filepath.ToSlash(strings.TrimPrefix(pkgInfo.Dir, p.Path))
if result == "" {
return "main"
}
// Remove the leading slash
if result[0] == '/' || result[0] == '\\' {
result = result[1:]
}
return result
}
func (p *Project) parseTypes(pkgs map[string]*ParsedPackage) {
@@ -1126,6 +1169,56 @@ func (p *Project) parseTypes(pkgs map[string]*ParsedPackage) {
}
}
func (p *Project) RelativeBindingsDir(dir *ParsedPackage, dir2 *ParsedPackage) string {
if dir.Dir == dir2.Dir {
return "."
}
// Calculate the relative path from the bindings directory to the package directory
absoluteSourceDir := dir.Dir
if absoluteSourceDir == p.Path {
absoluteSourceDir = filepath.Join(p.Path, p.outputDirectory, "main")
} else {
absoluteSourceDir = dir.Dir
}
targetRelativeDir := strings.TrimPrefix(dir2.Dir, p.Path)
targetBindingsDir := filepath.Join(p.Path, p.outputDirectory, targetRelativeDir)
// Calculate the relative path from the source directory to the target directory
relativePath, err := filepath.Rel(absoluteSourceDir, targetBindingsDir)
if err != nil {
panic(err)
}
return filepath.ToSlash(relativePath)
}
type ImportDef struct {
Name string
Path string
VarName string
PackageName string
}
func (p *Project) calculateImports(pkg string, m map[structName]*StructDef) []*ImportDef {
var result []*ImportDef
for _, structDef := range m {
for _, field := range structDef.Fields {
if field.Type.Package != pkg {
// Find the relative path from the source directory to the target directory
fieldPkgInfo := p.packageCache[field.Type.Package]
relativePath := p.RelativeBindingsDir(p.packageCache[pkg], fieldPkgInfo)
result = append(result, &ImportDef{
PackageName: fieldPkgInfo.Name,
Name: field.Name,
Path: relativePath,
VarName: fieldPkgInfo.Name + field.Name,
})
}
}
}
return result
}
func getTypeString(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
+38 -130
View File
@@ -87,13 +87,14 @@ func TestParseEnum(t *testing.T) {
wantTypes: map[string]map[string]*TypeDef{
"main": {
"Title": {
Name: "Title",
Type: "string",
Name: "Title",
DocComments: []string{"// Title is a title"},
Type: "string",
Consts: []*ConstDef{
{
Name: "Mister",
DocComment: "Mister is a title",
Value: `"Mr"`,
Name: "Mister",
DocComments: []string{"// Mister is a title"},
Value: `"Mr"`,
},
{
Name: "Miss",
@@ -119,131 +120,8 @@ func TestParseEnum(t *testing.T) {
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",
},
},
},
},
},
},
},
{
name: "should find a bound service with an enum interface",
dir: "testdata/enum-interface",
wantErr: false,
wantBoundMethods: map[string]map[string][]*BoundMethod{
"main": {
"GreetService": {
{
Package: "main",
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,
},
{
Package: "main",
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",
Name: "Person",
DocComments: []string{"// Person represents a person"},
Fields: []*Field{
{
Name: "Title",
@@ -273,6 +151,36 @@ func TestParseEnum(t *testing.T) {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Patch the PackageDir in the wantBoundMethods
for _, packageData := range got.BoundMethods {
for _, boundMethods := range packageData {
for _, boundMethod := range boundMethods {
boundMethod.PackageDir = ""
}
}
}
// Loop over the things we want
for packageName, packageData := range tt.wantBoundMethods {
for structName, wantBoundMethods := range packageData {
gotBoundMethods := got.BoundMethods[packageName][structName]
if diff := cmp.Diff(wantBoundMethods, gotBoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
}
}
// Loop over the models
for _, packageData := range got.Models {
for _, wantModel := range packageData {
// Loop over the Fields
for _, field := range wantModel.Fields {
field.Project = nil
}
}
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
+47 -13
View File
@@ -111,22 +111,23 @@ func TestParseFunction(t *testing.T) {
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services": {
"github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/services": {
"OtherService": {
{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "Yay",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/services",
Name: "Yay",
DocComment: "Yay does this and that",
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",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/services",
},
},
},
ID: 302702907,
ID: 1592414782,
},
},
},
@@ -134,7 +135,8 @@ func TestParseFunction(t *testing.T) {
wantModels: map[string]map[string]*StructDef{
"main": {
"Person": {
Name: "Person",
Name: "Person",
DocComments: []string{"// Person is a person"},
Fields: []*Field{
{
Name: "Name",
@@ -149,34 +151,34 @@ func TestParseFunction(t *testing.T) {
Name: "Address",
IsStruct: true,
IsPointer: true,
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/services",
},
},
},
},
},
"github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services": {
"github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/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",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/services",
Name: "string",
},
},
{
Name: "State",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/services",
Name: "string",
},
},
{
Name: "Country",
Type: &ParameterType{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/function_from_imported_package/services",
Name: "string",
},
},
@@ -193,8 +195,40 @@ func TestParseFunction(t *testing.T) {
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)
// Patch the PackageDir in the wantBoundMethods
for _, packageData := range got.BoundMethods {
for _, boundMethods := range packageData {
for _, boundMethod := range boundMethods {
boundMethod.PackageDir = ""
}
}
}
// Loop over the things we want
for packageName, packageData := range tt.wantBoundMethods {
for structName, wantBoundMethods := range packageData {
gotBoundMethods := got.BoundMethods[packageName][structName]
if diff := cmp.Diff(wantBoundMethods, gotBoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
}
}
// Loop over the models
for _, packageData := range got.Models {
for _, wantModel := range packageData {
// Loop over the Fields
for _, field := range wantModel.Fields {
field.Project = nil
}
}
}
if !reflect.DeepEqual(tt.wantBoundMethods, got.BoundMethods) {
t.Errorf("ParseDirectory() failed:\n" + cmp.Diff(tt.wantBoundMethods, got.BoundMethods))
//spew.Dump(tt.wantBoundMethods)
//spew.Dump(got.BoundMethods)
}
if !reflect.DeepEqual(tt.wantModels, got.Models) {
t.Errorf("ParseDirectory() failed:\n" + cmp.Diff(tt.wantModels, got.Models))
@@ -163,8 +163,9 @@ func TestParseStructLiteralMultiple(t *testing.T) {
"github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services": {
"OtherService": {
{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services",
Name: "Yay",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services",
Name: "Yay",
DocComment: "Yay does this and that",
Outputs: []*Parameter{
{
Type: &ParameterType{
@@ -242,6 +243,36 @@ func TestParseStructLiteralMultiple(t *testing.T) {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Patch the PackageDir in the wantBoundMethods
for _, packageData := range got.BoundMethods {
for _, boundMethods := range packageData {
for _, boundMethod := range boundMethods {
boundMethod.PackageDir = ""
}
}
}
// Loop over the things we want
for packageName, packageData := range tt.wantBoundMethods {
for structName, wantBoundMethods := range packageData {
gotBoundMethods := got.BoundMethods[packageName][structName]
if diff := cmp.Diff(wantBoundMethods, gotBoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
}
}
// Loop over the models
for _, packageData := range got.Models {
for _, wantModel := range packageData {
// Loop over the Fields
for _, field := range wantModel.Fields {
field.Project = nil
}
}
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
@@ -1119,6 +1119,36 @@ func TestParseStructLiteralNonPointerSingle(t *testing.T) {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Patch the PackageDir in the wantBoundMethods
for _, packageData := range got.BoundMethods {
for _, boundMethods := range packageData {
for _, boundMethod := range boundMethods {
boundMethod.PackageDir = ""
}
}
}
// Loop over the things we want
for packageName, packageData := range tt.wantBoundMethods {
for structName, wantBoundMethods := range packageData {
gotBoundMethods := got.BoundMethods[packageName][structName]
if diff := cmp.Diff(wantBoundMethods, gotBoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
}
}
// Loop over the models
for _, packageData := range got.Models {
for _, wantModel := range packageData {
// Loop over the Fields
for _, field := range wantModel.Fields {
field.Project = nil
}
}
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
@@ -1120,6 +1120,36 @@ func TestParseStructLiteralSingle(t *testing.T) {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Patch the PackageDir in the wantBoundMethods
for _, packageData := range got.BoundMethods {
for _, boundMethods := range packageData {
for _, boundMethod := range boundMethods {
boundMethod.PackageDir = ""
}
}
}
// Loop over the things we want
for packageName, packageData := range tt.wantBoundMethods {
for structName, wantBoundMethods := range packageData {
gotBoundMethods := got.BoundMethods[packageName][structName]
if diff := cmp.Diff(wantBoundMethods, gotBoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
}
}
// Loop over the models
for _, packageData := range got.Models {
for _, wantModel := range packageData {
// Loop over the Fields
for _, field := range wantModel.Fields {
field.Project = nil
}
}
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
@@ -149,8 +149,9 @@ func TestParseVariableSingle(t *testing.T) {
"github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services": {
"OtherService": {
{
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "Yay",
Package: "github.com/wailsapp/wails/v3/internal/parser/testdata/variable_single_from_other_function/services",
Name: "Yay",
DocComment: "Yay does this and that",
Outputs: []*Parameter{
{
Type: &ParameterType{
@@ -169,7 +170,8 @@ func TestParseVariableSingle(t *testing.T) {
wantModels: map[string]map[string]*StructDef{
"main": {
"Person": {
Name: "Person",
Name: "Person",
DocComments: []string{"// Person is a person!", "// They have a name and an address"},
Fields: []*Field{
{
Name: "Name",
@@ -228,6 +230,36 @@ func TestParseVariableSingle(t *testing.T) {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Patch the PackageDir in the wantBoundMethods
for _, packageData := range got.BoundMethods {
for _, boundMethods := range packageData {
for _, boundMethod := range boundMethods {
boundMethod.PackageDir = ""
}
}
}
// Loop over the things we want
for packageName, packageData := range tt.wantBoundMethods {
for structName, wantBoundMethods := range packageData {
gotBoundMethods := got.BoundMethods[packageName][structName]
if diff := cmp.Diff(wantBoundMethods, gotBoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
}
}
// Loop over the models
for _, packageData := range got.Models {
for _, wantModel := range packageData {
// Loop over the Fields
for _, field := range wantModel.Fields {
field.Project = nil
}
}
}
if diff := cmp.Diff(tt.wantBoundMethods, got.BoundMethods); diff != "" {
t.Errorf("ParseDirectory() failed:\n" + diff)
}
+24 -18
View File
@@ -1,20 +1,26 @@
{{$pkg := .Package}}
export namespace {{.Package}} {
{{range $enumindex, $enumdef := .Enums}}
{{- range $commentindex, $commentdef := .DocComments}}
{{$commentdef -}}
{{- $pkg := .Package}}{{- range .Imports}}
import * as {{.PackageName}} from "{{.Path}}/models";{{- end}}
{{- range $enumindex, $enumdef := .Enums}}
{{- range $commentindex, $commentdef := $enumdef.DocComments}}
{{$commentdef -}}
{{- end}}
export enum {{$enumdef.Name}} {
{{- range $constindex, $constdef := .Consts}}
{{- if $constdef.DocComments}}
{{- range $commentindex, $commentdef := $constdef.DocComments}}
{{$commentdef -}}
{{- end }}
{{- 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 interface {{$def.Name}} { {{range $def.Fields}}
{{.JSDef $pkg}}{{end}}
}
{{end}}
{{$constdef.Name}} = {{$constdef.Value}},{{end}}
}
{{- end}}
{{range $name, $def := .Models}}
{{- if $def.DocComments}}
{{- range $commentindex, $commentdef := $def.DocComments}}
{{$commentdef -}}
{{- end }}
{{- end}}
export interface {{$def.Name}} { {{- range $def.Fields}}
{{.JSDef $pkg}}{{end}}
}
{{end}}
+26 -15
View File
@@ -1,21 +1,32 @@
{{$pkg := .Package}}
// Defining the {{$pkg}} namespace
export const {{$pkg}} = {};
{{range $enumindex, $enumdef := .Enums}}
// Simulating the enum with an object
{{$pkg}}.{{$enumdef.Name}} = {
{{- if .Imports }}
/**
{{- range .Imports}}
* @typedef {import('{{.Path}}/models').{{.Name -}} } {{.VarName}}
{{- end}}
*/
{{end}}
{{- range $enumindex, $enumdef := .Enums}}
{{- range $commentindex, $commentdef := $enumdef.DocComments}}
{{$commentdef -}}
{{- end}}
export const {{$enumdef.Name}} = {
{{- range $constindex, $constdef := .Consts}}
{{- if $constdef.DocComment}}
// {{$constdef.DocComment}}
{{- if $constdef.DocComments}}
{{- range $commentindex, $commentdef := $constdef.DocComments}}
{{$commentdef -}}
{{- end }}
{{- end}}
{{$constdef.Name}}: {{$constdef.Value}},{{end}}
};
{{end}}
{{- range $name, $def := .Models}}
{{- if $def.DocComments}}
{{- range $commentindex, $commentdef := $def.DocComments}}
{{$commentdef -}}
{{- end }}
{{- end}}
{{range $name, $def := .Models}}
{{- if $def.DocComment}}
// {{$def.DocComment}}
{{- end -}}
{{$pkg}}.{{$def.Name}} = class {
export const {{$def.Name}} = class {
/**
* Creates a new {{$def.Name}} instance.
* @constructor
@@ -31,11 +42,11 @@ export const {{$pkg}} = {};
/**
* 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.
* @returns { {{- $def.Name -}} } A new {{$def.Name}} instance.
*/
static createFrom(source = {}) {
static createFrom(source) {
let parsedSource = typeof source === 'string' ? JSON.parse(source) : source;
return new {{$pkg}}.{{$def.Name}}(parsedSource);
return new {{$def.Name}}(parsedSource);
}
};
{{end}}
+35 -28
View File
@@ -1,31 +1,38 @@
{{$pkg := .Package}}
export namespace {{.Package}} {
{{range $enumindex, $enumdef := .Enums}}
{{- range $commentindex, $commentdef := .DocComments}}
{{$commentdef -}}
{{- $pkg := .Package}}{{- range .Imports}}
import * as {{.PackageName}} from "{{.Path}}/models";{{- end}}
{{- range $enumindex, $enumdef := .Enums}}
{{- range $commentindex, $commentdef := $enumdef.DocComments}}
{{$commentdef -}}
{{- end}}
export enum {{$enumdef.Name}} {
{{- range $constindex, $constdef := .Consts}}
{{- if $constdef.DocComments}}
{{- range $commentindex, $commentdef := $constdef.DocComments}}
{{$commentdef -}}
{{- end }}
{{- 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}}>);
}
{{$constdef.Name}} = {{$constdef.Value}},{{end}}
}
{{- end}}
{{range $name, $def := .Models}}
{{- if $def.DocComments}}
{{- range $commentindex, $commentdef := $def.DocComments}}
{{$commentdef -}}
{{- end }}
{{- end}}
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}}
}
{{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}}
@@ -1,29 +0,0 @@
// @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)); },
};
@@ -1,29 +0,0 @@
// @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.CallByName("main.GreetService.Greet", ...Array.prototype.slice.call(arguments, 0)); },
/**
* GreetService.NewPerson
* NewPerson creates a new person
* @param name {string}
* @returns {Promise<mainPerson>}
**/
NewPerson: function(name) { return wails.CallByName("main.GreetService.NewPerson", ...Array.prototype.slice.call(arguments, 0)); },
};

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