Files

373 lines
9.4 KiB
Go
Raw Permalink Normal View History

2019-10-08 06:12:08 +11:00
package binding
2019-01-08 07:58:46 +11:00
import (
"fmt"
2020-02-08 09:58:16 +11:00
"io/ioutil"
"os"
"path/filepath"
2019-01-08 07:58:46 +11:00
"reflect"
2020-02-08 09:58:16 +11:00
"strings"
2019-01-08 07:58:46 +11:00
"unicode"
2019-10-08 06:12:08 +11:00
"github.com/wailsapp/wails/lib/interfaces"
"github.com/wailsapp/wails/lib/logger"
"github.com/wailsapp/wails/lib/messages"
2019-01-08 07:58:46 +11:00
)
2020-02-08 09:58:16 +11:00
var typescriptDefinitionFilename = ""
2019-10-08 06:12:08 +11:00
// Manager handles method binding
type Manager struct {
2019-01-08 07:58:46 +11:00
methods map[string]*boundMethod
functions map[string]*boundFunction
internalMethods *internalMethods
2019-01-08 07:58:46 +11:00
initMethods []*boundMethod
2019-10-23 14:08:56 +11:00
shutdownMethods []*boundMethod
2019-10-08 06:12:08 +11:00
log *logger.CustomLogger
renderer interfaces.Renderer
runtime interfaces.Runtime // The runtime object to pass to bound structs
2019-01-08 07:58:46 +11:00
objectsToBind []interface{}
2020-02-08 09:58:16 +11:00
bindPackageNames bool // Package name should be considered when binding
structList map[string][]string // structList["mystruct"] = []string{"Method1", "Method2"}
2019-01-08 07:58:46 +11:00
}
2019-10-08 06:12:08 +11:00
// NewManager creates a new Manager struct
func NewManager() interfaces.BindingManager {
2020-02-08 09:58:16 +11:00
2019-10-08 06:12:08 +11:00
result := &Manager{
methods: make(map[string]*boundMethod),
functions: make(map[string]*boundFunction),
2019-10-08 06:12:08 +11:00
log: logger.NewCustomLogger("Bind"),
internalMethods: newInternalMethods(),
2020-02-08 09:58:16 +11:00
structList: make(map[string][]string),
2019-01-08 07:58:46 +11:00
}
return result
}
2019-10-08 06:12:08 +11:00
// BindPackageNames sets a flag to indicate package names should be considered when binding
func (b *Manager) BindPackageNames() {
2019-01-08 07:58:46 +11:00
b.bindPackageNames = true
}
2019-10-08 06:12:08 +11:00
// Start the binding manager
func (b *Manager) Start(renderer interfaces.Renderer, runtime interfaces.Runtime) error {
2019-01-08 07:58:46 +11:00
b.log.Info("Starting")
b.renderer = renderer
b.runtime = runtime
err := b.initialise()
if err != nil {
b.log.Errorf("Binding error: %s", err.Error())
return err
}
err = b.callWailsInitMethods()
return err
}
2019-10-08 06:12:08 +11:00
func (b *Manager) initialise() error {
2019-01-08 07:58:46 +11:00
var err error
// var binding *boundMethod
b.log.Info("Binding Go Functions/Methods")
// Create bindings for objects
for _, object := range b.objectsToBind {
// Safeguard against nils
if object == nil {
return fmt.Errorf("attempted to bind nil object")
}
// Determine kind of object
objectType := reflect.TypeOf(object)
objectKind := objectType.Kind()
switch objectKind {
case reflect.Ptr:
err = b.bindMethod(object)
case reflect.Func:
// spew.Dump(result.objectType.String())
err = b.bindFunction(object)
default:
err = fmt.Errorf("cannot bind object of type '%s'", objectKind.String())
}
// Return error if set
if err != nil {
return err
}
}
2020-02-08 09:58:16 +11:00
// If we wish to generate a typescript definition file...
if typescriptDefinitionFilename != "" {
err := b.generateTypescriptDefinitions()
if err != nil {
return err
}
}
2019-01-08 07:58:46 +11:00
return nil
}
2020-02-08 09:58:16 +11:00
// Generate typescript
func (b *Manager) generateTypescriptDefinitions() error {
var output strings.Builder
for structname, methodList := range b.structList {
structname = strings.SplitN(structname, ".", 2)[1]
output.WriteString(fmt.Sprintf("interface %s {\n", structname))
2020-02-08 09:58:16 +11:00
for _, method := range methodList {
output.WriteString(fmt.Sprintf("\t%s(...args : any[]):Promise<any>\n", method))
2020-02-08 09:58:16 +11:00
}
output.WriteString("}\n")
}
output.WriteString("\n")
output.WriteString("interface Backend {\n")
2020-02-08 09:58:16 +11:00
for structname := range b.structList {
structname = strings.SplitN(structname, ".", 2)[1]
output.WriteString(fmt.Sprintf("\t%[1]s: %[1]s\n", structname))
}
output.WriteString("}\n")
globals := `
declare global {
interface Window {
backend: Backend;
}
}
export {};`
2020-02-08 09:58:16 +11:00
output.WriteString(globals)
b.log.Info("Written Typescript file: " + typescriptDefinitionFilename)
dir := filepath.Dir(typescriptDefinitionFilename)
os.MkdirAll(dir, 0755)
return ioutil.WriteFile(typescriptDefinitionFilename, []byte(output.String()), 0755)
}
2019-01-08 07:58:46 +11:00
// bind the given struct method
2019-10-08 06:12:08 +11:00
func (b *Manager) bindMethod(object interface{}) error {
2019-01-08 07:58:46 +11:00
objectType := reflect.TypeOf(object)
baseName := objectType.String()
// Strip pointer if there
if baseName[0] == '*' {
baseName = baseName[1:]
}
b.log.Debugf("Processing struct: %s", baseName)
2020-02-08 09:58:16 +11:00
// Calc actual name
actualName := strings.TrimPrefix(baseName, "main.")
if b.structList[actualName] == nil {
b.structList[actualName] = []string{}
}
2019-01-08 07:58:46 +11:00
// Iterate over method definitions
for i := 0; i < objectType.NumMethod(); i++ {
// Get method definition
methodDef := objectType.Method(i)
methodName := methodDef.Name
fullMethodName := baseName + "." + methodName
method := reflect.ValueOf(object).MethodByName(methodName)
2020-02-08 09:58:16 +11:00
b.structList[actualName] = append(b.structList[actualName], methodName)
2019-01-08 07:58:46 +11:00
// Skip unexported methods
if !unicode.IsUpper([]rune(methodName)[0]) {
continue
}
// Create a new boundMethod
newMethod, err := newBoundMethod(methodName, fullMethodName, method, objectType)
if err != nil {
return err
}
// Check if it's a wails init function
if newMethod.isWailsInit {
b.log.Debugf("Detected WailsInit function: %s", fullMethodName)
b.initMethods = append(b.initMethods, newMethod)
2019-10-23 14:08:56 +11:00
} else if newMethod.isWailsShutdown {
b.log.Debugf("Detected WailsShutdown function: %s", fullMethodName)
b.shutdownMethods = append(b.shutdownMethods, newMethod)
2019-01-08 07:58:46 +11:00
} else {
// Save boundMethod
b.log.Infof("Bound Method: %s()", fullMethodName)
b.methods[fullMethodName] = newMethod
// Inform renderer of new binding
b.renderer.NewBinding(fullMethodName)
}
}
return nil
}
// bind the given function object
2019-10-08 06:12:08 +11:00
func (b *Manager) bindFunction(object interface{}) error {
2019-01-08 07:58:46 +11:00
newFunction, err := newBoundFunction(object)
if err != nil {
return err
}
// Save method
b.log.Infof("Bound Function: %s()", newFunction.fullName)
b.functions[newFunction.fullName] = newFunction
// Register with Renderer
b.renderer.NewBinding(newFunction.fullName)
return nil
}
2019-10-08 06:12:08 +11:00
// Bind saves the given object to be bound at start time
func (b *Manager) Bind(object interface{}) {
2019-01-08 07:58:46 +11:00
// Store binding
b.objectsToBind = append(b.objectsToBind, object)
}
2019-10-08 06:12:08 +11:00
func (b *Manager) processInternalCall(callData *messages.CallData) (interface{}, error) {
// Strip prefix
return b.internalMethods.processCall(callData)
}
2019-10-08 06:12:08 +11:00
func (b *Manager) processFunctionCall(callData *messages.CallData) (interface{}, error) {
2019-02-04 19:50:26 +11:00
// Return values
var result []reflect.Value
var err error
function := b.functions[callData.BindingName]
if function == nil {
return nil, fmt.Errorf("Invalid function name '%s'", callData.BindingName)
}
result, err = function.call(callData.Data)
if err != nil {
return nil, err
}
// Do we have an error return type?
if function.hasErrorReturnType {
// We do - last result is an error type
// Check if the last result was nil
b.log.Debugf("# of return types: %d", len(function.returnTypes))
b.log.Debugf("# of results: %d", len(result))
errorResult := result[len(function.returnTypes)-1]
if !errorResult.IsNil() {
// It wasn't - we have an error
return nil, errorResult.Interface().(error)
}
}
2019-10-08 06:12:08 +11:00
// fmt.Printf("result = '%+v'\n", result)
if len(result) > 0 {
return result[0].Interface(), nil
}
return nil, nil
2019-02-04 19:50:26 +11:00
}
2019-10-08 06:12:08 +11:00
func (b *Manager) processMethodCall(callData *messages.CallData) (interface{}, error) {
2019-02-04 19:50:26 +11:00
// Return values
var result []reflect.Value
var err error
// do we have this method?
method := b.methods[callData.BindingName]
if method == nil {
return nil, fmt.Errorf("Invalid method name '%s'", callData.BindingName)
}
result, err = method.call(callData.Data)
if err != nil {
return nil, err
}
// Do we have an error return type?
if method.hasErrorReturnType {
// We do - last result is an error type
// Check if the last result was nil
b.log.Debugf("# of return types: %d", len(method.returnTypes))
b.log.Debugf("# of results: %d", len(result))
errorResult := result[len(method.returnTypes)-1]
if !errorResult.IsNil() {
// It wasn't - we have an error
return nil, errorResult.Interface().(error)
}
}
if result != nil {
return result[0].Interface(), nil
}
return nil, nil
}
2019-10-08 06:12:08 +11:00
// ProcessCall processes the given call request
func (b *Manager) ProcessCall(callData *messages.CallData) (result interface{}, err error) {
2019-01-08 07:58:46 +11:00
b.log.Debugf("Wanting to call %s", callData.BindingName)
// Determine if this is function call or method call by the number of
// dots in the binding name
dotCount := 0
for _, character := range callData.BindingName {
if character == '.' {
dotCount++
}
}
// We need to catch reflect related panics and return
// a decent error message
// TODO: DEBUG THIS!
2019-02-04 19:50:26 +11:00
2019-01-08 07:58:46 +11:00
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%s", r.(string))
}
}()
switch dotCount {
case 1:
2019-02-04 19:50:26 +11:00
result, err = b.processFunctionCall(callData)
2019-01-08 07:58:46 +11:00
case 2:
2019-02-04 19:50:26 +11:00
result, err = b.processMethodCall(callData)
case 3:
result, err = b.processInternalCall(callData)
2019-01-08 07:58:46 +11:00
default:
2019-02-04 19:50:26 +11:00
result = nil
err = fmt.Errorf("Invalid binding name '%s'", callData.BindingName)
2019-01-08 07:58:46 +11:00
}
2019-02-04 19:50:26 +11:00
return
2019-01-08 07:58:46 +11:00
}
// callWailsInitMethods calls all of the WailsInit methods that were
// registered with the runtime object
2019-10-08 06:12:08 +11:00
func (b *Manager) callWailsInitMethods() error {
2019-01-08 07:58:46 +11:00
// Create reflect value for runtime object
runtimeValue := reflect.ValueOf(b.runtime)
params := []reflect.Value{runtimeValue}
// Iterate initMethods
for _, initMethod := range b.initMethods {
// Call
result := initMethod.method.Call(params)
// Check errors
err := result[0].Interface()
if err != nil {
return err.(error)
}
}
return nil
}
2019-10-23 14:08:56 +11:00
// Shutdown the binding manager
func (b *Manager) Shutdown() {
b.log.Debug("Shutdown called")
for _, method := range b.shutdownMethods {
b.log.Debugf("Calling Shutdown for method: %s", method.fullName)
method.call("[]")
}
b.log.Debug("Shutdown complete")
}