5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 07:09:54 +08:00
wails/v2/internal/binding/binding.go
Jeremy Jay c4fdfd6415
Fix miscellaneous bindings and typescript export bugs (#3978)
* Do not attempt to export fields that cannot be json-encoded

* update changelog w/ PR

* also skip UnsafePointers

* WIP to allow conversion from Go generic types to typescript

* support for non-primitive generics also :)

* fix generic types in parameters / return args

* fixes a namespacing bug when mapping to pointer to struct

* fixing invalid knownstructs

* found a place it mattered, pushing the star replacement to the generate side

* descend as much as necessary to find structs

caught these examples in http.Request.TLS:

PeerCertificates []*x509.Certificate
VerifiedChains [][]*x509.Certificate

* accidently reverted other fix

* switch syntax for typescript record outputs

prior syntax is primarily useful for naming keys
so not useful here, and this syntax avoids square
brackets in output which greatly simplifies
generation for Go generics

* better handle edge cases for nested arrays and slices

* lots o tests

* update changelog

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
2025-01-13 20:14:54 +11:00

382 lines
9.6 KiB
Go

package binding
import (
"bufio"
"bytes"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"github.com/wailsapp/wails/v2/internal/typescriptify"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/logger"
)
type Bindings struct {
db *DB
logger logger.CustomLogger
exemptions slicer.StringSlicer
structsToGenerateTS map[string]map[string]interface{}
enumsToGenerateTS map[string]map[string]interface{}
tsPrefix string
tsSuffix string
tsInterface bool
obfuscate bool
}
// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings {
result := &Bindings{
db: newDB(),
logger: logger.CustomLogger("Bindings"),
structsToGenerateTS: make(map[string]map[string]interface{}),
enumsToGenerateTS: make(map[string]map[string]interface{}),
obfuscate: obfuscate,
}
for _, exemption := range exemptions {
if exemption == nil {
continue
}
name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name()
// Yuk yuk yuk! Is there a better way?
name = strings.TrimSuffix(name, "-fm")
result.exemptions.Add(name)
}
for _, enum := range enumsToBind {
result.AddEnumToGenerateTS(enum)
}
// Add the structs to bind
for _, ptr := range structPointersToBind {
err := result.Add(ptr)
if err != nil {
logger.Fatal("Error during binding: " + err.Error())
}
}
return result
}
// Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error {
methods, err := b.getMethods(structPtr)
if err != nil {
return fmt.Errorf("cannot bind value to app: %s", err.Error())
}
for _, method := range methods {
splitName := strings.Split(method.Name, ".")
packageName := splitName[0]
structName := splitName[1]
methodName := splitName[2]
// Add it as a regular method
b.db.AddMethod(packageName, structName, methodName, method)
}
return nil
}
func (b *Bindings) DB() *DB {
return b.db
}
func (b *Bindings) ToJSON() (string, error) {
return b.db.ToJSON()
}
func (b *Bindings) GenerateModels() ([]byte, error) {
models := map[string]string{}
var seen slicer.StringSlicer
var seenEnumsPackages slicer.StringSlicer
allStructNames := b.getAllStructNames()
allStructNames.Sort()
allEnumNames := b.getAllEnumNames()
allEnumNames.Sort()
for packageName, structsToGenerate := range b.structsToGenerateTS {
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
w.KnownStructs = allStructNames
w.KnownEnums = allEnumNames
// sort the structs
var structNames []string
for structName := range structsToGenerate {
structNames = append(structNames, structName)
}
sort.Strings(structNames)
for _, structName := range structNames {
fqstructname := packageName + "." + structName
if seen.Contains(fqstructname) {
continue
}
structInterface := structsToGenerate[structName]
w.Add(structInterface)
}
// if we have enums for this package, add them as well
var enums, enumsExist = b.enumsToGenerateTS[packageName]
if enumsExist {
for enumName, enum := range enums {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
seenEnumsPackages.Add(packageName)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
seen.AddSlice(w.GetGeneratedStructs())
models[packageName] = thisPackageCode
}
// Add outstanding enums to the models that were not in packages with structs
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
if seenEnumsPackages.Contains(packageName) {
continue
}
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
for enumName, enum := range enumsToGenerate {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
models[packageName] = thisPackageCode
}
// Sort the package names first to make the output deterministic
sortedPackageNames := make([]string, 0)
for packageName := range models {
sortedPackageNames = append(sortedPackageNames, packageName)
}
sort.Strings(sortedPackageNames)
var modelsData bytes.Buffer
for _, packageName := range sortedPackageNames {
modelData := models[packageName]
if strings.TrimSpace(modelData) == "" {
continue
}
modelsData.WriteString("export namespace " + packageName + " {\n")
sc := bufio.NewScanner(strings.NewReader(modelData))
for sc.Scan() {
modelsData.WriteString("\t" + sc.Text() + "\n")
}
modelsData.WriteString("\n}\n\n")
}
return modelsData.Bytes(), nil
}
func (b *Bindings) WriteModels(modelsDir string) error {
modelsData, err := b.GenerateModels()
if err != nil {
return err
}
// Don't write if we don't have anything
if len(modelsData) == 0 {
return nil
}
filename := filepath.Join(modelsDir, "models.ts")
err = os.WriteFile(filename, modelsData, 0o755)
if err != nil {
return err
}
return nil
}
func (b *Bindings) AddEnumToGenerateTS(e interface{}) {
enumType := reflect.TypeOf(e)
var packageName string
var enumName string
// enums should be represented as array of all possible values
if hasElements(enumType) {
enum := enumType.Elem()
// simple enum represented by struct with Value/TSName fields
if enum.Kind() == reflect.Struct {
_, tsNamePresented := enum.FieldByName("TSName")
enumT, valuePresented := enum.FieldByName("Value")
if tsNamePresented && valuePresented {
packageName = getPackageName(enumT.Type.String())
enumName = enumT.Type.Name()
} else {
return
}
// otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname
} else {
packageName = getPackageName(enumType.Elem().String())
enumName = enumType.Elem().Name()
}
if b.enumsToGenerateTS[packageName] == nil {
b.enumsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.enumsToGenerateTS[packageName][enumName] != nil {
return
}
b.enumsToGenerateTS[packageName][enumName] = e
}
}
func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) {
if b.structsToGenerateTS[packageName] == nil {
b.structsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.structsToGenerateTS[packageName][structName] != nil {
return
}
b.structsToGenerateTS[packageName][structName] = s
// Iterate this struct and add any struct field references
structType := reflect.TypeOf(s)
for hasElements(structType) {
structType = structType.Elem()
}
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
if field.Anonymous || !field.IsExported() {
continue
}
kind := field.Type.Kind()
if kind == reflect.Struct {
fqname := field.Type.String()
sNameSplit := strings.SplitN(fqname, ".", 2)
if len(sNameSplit) < 2 {
continue
}
sName := sNameSplit[1]
pName := getPackageName(fqname)
a := reflect.New(field.Type)
if b.hasExportedJSONFields(field.Type) {
s := reflect.Indirect(a).Interface()
b.AddStructToGenerateTS(pName, sName, s)
}
} else {
fType := field.Type
for hasElements(fType) {
fType = fType.Elem()
}
if fType.Kind() == reflect.Struct {
fqname := fType.String()
sNameSplit := strings.SplitN(fqname, ".", 2)
if len(sNameSplit) < 2 {
continue
}
sName := sNameSplit[1]
pName := getPackageName(fqname)
a := reflect.New(fType)
if b.hasExportedJSONFields(fType) {
s := reflect.Indirect(a).Interface()
b.AddStructToGenerateTS(pName, sName, s)
}
}
}
}
}
func (b *Bindings) SetTsPrefix(prefix string) *Bindings {
b.tsPrefix = prefix
return b
}
func (b *Bindings) SetTsSuffix(postfix string) *Bindings {
b.tsSuffix = postfix
return b
}
func (b *Bindings) SetOutputType(outputType string) *Bindings {
if outputType == "interfaces" {
b.tsInterface = true
}
return b
}
func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, structsToGenerate := range b.structsToGenerateTS {
for structName := range structsToGenerate {
result.Add(packageName + "." + structName)
}
}
return &result
}
func (b *Bindings) getAllEnumNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
for enumName := range enumsToGenerate {
result.Add(packageName + "." + enumName)
}
}
return &result
}
func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool {
for i := 0; i < typeOf.NumField(); i++ {
jsonFieldName := ""
f := typeOf.Field(i)
// function, complex, and channel types cannot be json-encoded
if f.Type.Kind() == reflect.Chan ||
f.Type.Kind() == reflect.Func ||
f.Type.Kind() == reflect.UnsafePointer ||
f.Type.Kind() == reflect.Complex128 ||
f.Type.Kind() == reflect.Complex64 {
continue
}
jsonTag, hasTag := f.Tag.Lookup("json")
if !hasTag && f.IsExported() {
return true
}
if len(jsonTag) == 0 {
continue
}
jsonTagParts := strings.Split(jsonTag, ",")
if len(jsonTagParts) > 0 {
jsonFieldName = jsonTagParts[0]
}
for _, t := range jsonTagParts {
if t == "-" {
continue
}
}
if jsonFieldName != "" {
return true
}
}
return false
}