mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 04:40:41 +08:00

* 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>
382 lines
9.6 KiB
Go
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
|
|
}
|