feat: major refactor

This commit is contained in:
Lea Anthony
2019-07-12 10:12:15 +10:00
parent caa1e04b5a
commit 8aa97f64ef
72 changed files with 9221 additions and 980 deletions

165
lib/binding/function.go Normal file
View File

@@ -0,0 +1,165 @@
package binding
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"runtime"
"github.com/wailsapp/wails/lib/logger"
)
type boundFunction struct {
fullName string
function reflect.Value
functionType reflect.Type
inputs []reflect.Type
returnTypes []reflect.Type
log *logger.CustomLogger
hasErrorReturnType bool
}
// Creates a new bound function based on the given method + type
func newBoundFunction(object interface{}) (*boundFunction, error) {
objectValue := reflect.ValueOf(object)
objectType := reflect.TypeOf(object)
name := runtime.FuncForPC(objectValue.Pointer()).Name()
result := &boundFunction{
fullName: name,
function: objectValue,
functionType: objectType,
log: logger.NewCustomLogger(name),
}
err := result.processParameters()
return result, err
}
func (b *boundFunction) processParameters() error {
// Param processing
functionType := b.functionType
// Input parameters
inputParamCount := functionType.NumIn()
if inputParamCount > 0 {
b.inputs = make([]reflect.Type, inputParamCount)
// We start at 1 as the first param is the struct
for index := 0; index < inputParamCount; index++ {
param := functionType.In(index)
name := param.Name()
kind := param.Kind()
b.inputs[index] = param
typ := param
index := index
b.log.DebugFields("Input param", logger.Fields{
"index": index,
"name": name,
"kind": kind,
"typ": typ,
})
}
}
// Process return/output declarations
returnParamsCount := functionType.NumOut()
// Guard against bad number of return types
switch returnParamsCount {
case 0:
case 1:
// Check if it's an error type
param := functionType.Out(0)
paramName := param.Name()
if paramName == "error" {
b.hasErrorReturnType = true
}
// Save return type
b.returnTypes = append(b.returnTypes, param)
case 2:
// Check the second return type is an error
secondParam := functionType.Out(1)
secondParamName := secondParam.Name()
if secondParamName != "error" {
return fmt.Errorf("last return type of method '%s' must be an error (got %s)", b.fullName, secondParamName)
}
// Check the second return type is an error
firstParam := functionType.Out(0)
firstParamName := firstParam.Name()
if firstParamName == "error" {
return fmt.Errorf("first return type of method '%s' must not be an error", b.fullName)
}
b.hasErrorReturnType = true
// Save return types
b.returnTypes = append(b.returnTypes, firstParam)
b.returnTypes = append(b.returnTypes, secondParam)
default:
return fmt.Errorf("cannot register method '%s' with %d return parameters. Please use up to 2", b.fullName, returnParamsCount)
}
return nil
}
// call the method with the given data
func (b *boundFunction) call(data string) ([]reflect.Value, error) {
// The data will be an array of values so we will decode the
// input data into
var jsArgs []interface{}
d := json.NewDecoder(bytes.NewBufferString(data))
// d.UseNumber()
err := d.Decode(&jsArgs)
if err != nil {
return nil, fmt.Errorf("Invalid data passed to method call: %s", err.Error())
}
// Check correct number of inputs
if len(jsArgs) != len(b.inputs) {
return nil, fmt.Errorf("Invalid number of parameters given to %s. Expected %d but got %d", b.fullName, len(b.inputs), len(jsArgs))
}
// Set up call
args := make([]reflect.Value, len(b.inputs))
for index := 0; index < len(b.inputs); index++ {
// Set the input values
value, err := b.setInputValue(index, b.inputs[index], jsArgs[index])
if err != nil {
return nil, err
}
args[index] = value
}
b.log.Debugf("Unmarshalled Args: %+v\n", jsArgs)
b.log.Debugf("Converted Args: %+v\n", args)
results := b.function.Call(args)
b.log.Debugf("results = %+v", results)
return results, nil
}
// Attempts to set the method input <typ> for parameter <index> with the given value <val>
func (b *boundFunction) setInputValue(index int, typ reflect.Type, val interface{}) (result reflect.Value, err error) {
// Catch type conversion panics thrown by convert
defer func() {
if r := recover(); r != nil {
// Modify error
err = fmt.Errorf("%s for parameter %d of function %s", r.(string)[23:], index+1, b.fullName)
}
}()
// Translate javascript null values
if val == nil {
result = reflect.Zero(typ)
} else {
result = reflect.ValueOf(val).Convert(typ)
}
return result, err
}

71
lib/binding/internal.go Normal file
View File

@@ -0,0 +1,71 @@
package binding
import (
"fmt"
"strings"
"github.com/wailsapp/wails/lib/logger"
"github.com/wailsapp/wails/lib/messages"
"github.com/wailsapp/wails/runtime/go/runtime"
)
type internalMethods struct {
log *logger.CustomLogger
browser *runtime.Browser
}
func newInternalMethods() *internalMethods {
return &internalMethods{
log: logger.NewCustomLogger("InternalCall"),
browser: runtime.NewBrowser(),
}
}
func (i *internalMethods) processCall(callData *messages.CallData) (interface{}, error) {
if !strings.HasPrefix(callData.BindingName, ".wails.") {
return nil, fmt.Errorf("Invalid call signature '%s'", callData.BindingName)
}
// Strip prefix
var splitCall = strings.Split(callData.BindingName, ".")[2:]
if len(splitCall) != 2 {
return nil, fmt.Errorf("Invalid call signature '%s'", callData.BindingName)
}
group := splitCall[0]
switch group {
case "Browser":
return i.processBrowserCommand(splitCall[1], callData.Data)
default:
return nil, fmt.Errorf("Unknown internal command group '%s'", group)
}
}
func (i *internalMethods) processBrowserCommand(command string, data interface{}) (interface{}, error) {
switch command {
case "OpenURL":
url := data.(string)
// Strip string quotes. Credit: https://stackoverflow.com/a/44222648
if url[0] == '"' {
url = url[1:]
}
if i := len(url) - 1; url[i] == '"' {
url = url[:i]
}
i.log.Debugf("Calling Browser.OpenURL with '%s'", url)
return nil, i.browser.OpenURL(url)
case "OpenFile":
filename := data.(string)
// Strip string quotes. Credit: https://stackoverflow.com/a/44222648
if filename[0] == '"' {
filename = filename[1:]
}
if i := len(filename) - 1; filename[i] == '"' {
filename = filename[:i]
}
i.log.Debugf("Calling Browser.OpenFile with '%s'", filename)
return nil, i.browser.OpenFile(filename)
default:
return nil, fmt.Errorf("Unknown Browser command '%s'", command)
}
}

290
lib/binding/manager.go Normal file
View File

@@ -0,0 +1,290 @@
package binding
import (
"fmt"
"reflect"
"unicode"
"github.com/wailsapp/wails/lib/logger"
"github.com/wailsapp/wails/lib/messages"
"github.com/wailsapp/wails/lib/interfaces"
)
// Manager handles method binding
type Manager struct {
methods map[string]*boundMethod
functions map[string]*boundFunction
internalMethods *internalMethods
initMethods []*boundMethod
log *logger.CustomLogger
renderer interfaces.Renderer
runtime interfaces.Runtime // The runtime object to pass to bound structs
objectsToBind []interface{}
bindPackageNames bool // Package name should be considered when binding
}
// NewManager creates a new Manager struct
func NewManager() interfaces.BindingManager {
result := &Manager{
methods: make(map[string]*boundMethod),
functions: make(map[string]*boundFunction),
log: logger.NewCustomLogger("Bind"),
internalMethods: newInternalMethods(),
}
return result
}
// BindPackageNames sets a flag to indicate package names should be considered when binding
func (b *Manager) BindPackageNames() {
b.bindPackageNames = true
}
// Start the binding manager
func (b *Manager) Start(renderer interfaces.Renderer, runtime interfaces.Runtime) error {
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
}
func (b *Manager) initialise() error {
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
}
}
return nil
}
// bind the given struct method
func (b *Manager) bindMethod(object interface{}) error {
objectType := reflect.TypeOf(object)
baseName := objectType.String()
// Strip pointer if there
if baseName[0] == '*' {
baseName = baseName[1:]
}
b.log.Debugf("Processing struct: %s", baseName)
// 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)
// 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)
} 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
func (b *Manager) bindFunction(object interface{}) error {
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
}
// Bind saves the given object to be bound at start time
func (b *Manager) Bind(object interface{}) {
// Store binding
b.objectsToBind = append(b.objectsToBind, object)
}
func (b *Manager) processInternalCall(callData *messages.CallData) (interface{}, error) {
// Strip prefix
return b.internalMethods.processCall(callData)
}
func (b *Manager) processFunctionCall(callData *messages.CallData) (interface{}, error) {
// 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)
}
}
return result[0].Interface(), nil
}
func (b *Manager) processMethodCall(callData *messages.CallData) (interface{}, error) {
// 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
}
// ProcessCall processes the given call request
func (b *Manager) ProcessCall(callData *messages.CallData) (result interface{}, err error) {
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!
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%s", r.(string))
}
}()
switch dotCount {
case 1:
result, err = b.processFunctionCall(callData)
case 2:
result, err = b.processMethodCall(callData)
case 3:
result, err = b.processInternalCall(callData)
default:
result = nil
err = fmt.Errorf("Invalid binding name '%s'", callData.BindingName)
}
return
}
// callWailsInitMethods calls all of the WailsInit methods that were
// registered with the runtime object
func (b *Manager) callWailsInitMethods() error {
// 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
}

213
lib/binding/method.go Normal file
View File

@@ -0,0 +1,213 @@
package binding
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"github.com/wailsapp/wails/lib/logger"
)
type boundMethod struct {
Name string
fullName string
method reflect.Value
inputs []reflect.Type
returnTypes []reflect.Type
log *logger.CustomLogger
hasErrorReturnType bool // Indicates if there is an error return type
isWailsInit bool
}
// Creates a new bound method based on the given method + type
func newBoundMethod(name string, fullName string, method reflect.Value, objectType reflect.Type) (*boundMethod, error) {
result := &boundMethod{
Name: name,
method: method,
fullName: fullName,
}
// Setup logger
result.log = logger.NewCustomLogger(result.fullName)
// Check if Parameters are valid
err := result.processParameters()
// Are we a WailsInit method?
if result.Name == "WailsInit" {
err = result.processWailsInit()
}
return result, err
}
func (b *boundMethod) processParameters() error {
// Param processing
methodType := b.method.Type()
// Input parameters
inputParamCount := methodType.NumIn()
if inputParamCount > 0 {
b.inputs = make([]reflect.Type, inputParamCount)
// We start at 1 as the first param is the struct
for index := 0; index < inputParamCount; index++ {
param := methodType.In(index)
name := param.Name()
kind := param.Kind()
b.inputs[index] = param
typ := param
index := index
b.log.DebugFields("Input param", logger.Fields{
"index": index,
"name": name,
"kind": kind,
"typ": typ,
})
}
}
// Process return/output declarations
returnParamsCount := methodType.NumOut()
// Guard against bad number of return types
switch returnParamsCount {
case 0:
case 1:
// Check if it's an error type
param := methodType.Out(0)
paramName := param.Name()
if paramName == "error" {
b.hasErrorReturnType = true
}
// Save return type
b.returnTypes = append(b.returnTypes, param)
case 2:
// Check the second return type is an error
secondParam := methodType.Out(1)
secondParamName := secondParam.Name()
if secondParamName != "error" {
return fmt.Errorf("last return type of method '%s' must be an error (got %s)", b.Name, secondParamName)
}
// Check the second return type is an error
firstParam := methodType.Out(0)
firstParamName := firstParam.Name()
if firstParamName == "error" {
return fmt.Errorf("first return type of method '%s' must not be an error", b.Name)
}
b.hasErrorReturnType = true
// Save return types
b.returnTypes = append(b.returnTypes, firstParam)
b.returnTypes = append(b.returnTypes, secondParam)
default:
return fmt.Errorf("cannot register method '%s' with %d return parameters. Please use up to 2", b.Name, returnParamsCount)
}
return nil
}
// call the method with the given data
func (b *boundMethod) call(data string) ([]reflect.Value, error) {
// The data will be an array of values so we will decode the
// input data into
var jsArgs []interface{}
d := json.NewDecoder(bytes.NewBufferString(data))
// d.UseNumber()
err := d.Decode(&jsArgs)
if err != nil {
return nil, fmt.Errorf("Invalid data passed to method call: %s", err.Error())
}
// Check correct number of inputs
if len(jsArgs) != len(b.inputs) {
return nil, fmt.Errorf("Invalid number of parameters given to %s. Expected %d but got %d", b.fullName, len(b.inputs), len(jsArgs))
}
// Set up call
args := make([]reflect.Value, len(b.inputs))
for index := 0; index < len(b.inputs); index++ {
// Set the input values
value, err := b.setInputValue(index, b.inputs[index], jsArgs[index])
if err != nil {
return nil, err
}
args[index] = value
}
b.log.Debugf("Unmarshalled Args: %+v\n", jsArgs)
b.log.Debugf("Converted Args: %+v\n", args)
results := b.method.Call(args)
b.log.Debugf("results = %+v", results)
return results, nil
}
// Attempts to set the method input <typ> for parameter <index> with the given value <val>
func (b *boundMethod) setInputValue(index int, typ reflect.Type, val interface{}) (result reflect.Value, err error) {
// Catch type conversion panics thrown by convert
defer func() {
if r := recover(); r != nil {
// Modify error
fmt.Printf("Recovery message: %+v\n", r)
err = fmt.Errorf("%s for parameter %d of method %s", r.(string)[23:], index+1, b.fullName)
}
}()
// Do the conversion
// Handle nil values
if val == nil {
switch typ.Kind() {
case reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Ptr,
reflect.Slice:
b.log.Debug("Converting nil to type")
result = reflect.ValueOf(val).Convert(typ)
default:
b.log.Debug("Cannot convert nil to type, returning error")
return reflect.Zero(typ), fmt.Errorf("Unable to use null value for parameter %d of method %s", index+1, b.fullName)
}
} else {
result = reflect.ValueOf(val).Convert(typ)
}
return result, err
}
func (b *boundMethod) processWailsInit() error {
// We must have only 1 input, it must be *wails.Runtime
if len(b.inputs) != 1 {
return fmt.Errorf("Invalid WailsInit() definition. Expected 1 input, but got %d", len(b.inputs))
}
// It must be *wails.Runtime
inputName := b.inputs[0].String()
b.log.Debugf("WailsInit input type: %s", inputName)
if inputName != "*wails.Runtime" {
return fmt.Errorf("Invalid WailsInit() definition. Expected input to be wails.Runtime, but got %s", inputName)
}
// We must have only 1 output, it must be error
if len(b.returnTypes) != 1 {
return fmt.Errorf("Invalid WailsInit() definition. Expected 1 return type, but got %d", len(b.returnTypes))
}
// It must be *wails.Runtime
outputName := b.returnTypes[0].String()
b.log.Debugf("WailsInit output type: %s", outputName)
if outputName != "error" {
return fmt.Errorf("Invalid WailsInit() definition. Expected input to be error, but got %s", outputName)
}
// We are indeed a wails Init method
b.isWailsInit = true
return nil
}