package wasm import ( "errors" "fmt" "reflect" "syscall/js" ) // ErrMultipleReturnValue is an error where a JS function is attempted to be unmarshalled into a Go function with // multiple return values. var ErrMultipleReturnValue = errors.New("a JS function can only return one value") // InvalidFromJSValueError is an error where an invalid argument is passed to FromJSValue. // The argument to Unmarshal must be a non-nil pointer. type InvalidFromJSValueError struct { Type reflect.Type } // Error implements error. func (e InvalidFromJSValueError) Error() string { return "invalid argument passed to FromJSValue. Got type " + e.Type.String() } // InvalidTypeError is an error where the JS value cannot be unmarshalled into the provided Go type. type InvalidTypeError struct { JSType js.Type GoType reflect.Type } // Error implements error. func (e InvalidTypeError) Error() string { return "invalid unmarshalling: cannot unmarshal " + e.JSType.String() + " into " + e.GoType.String() } // InvalidArrayError is an error where the JS's array length do not match Go's array length. type InvalidArrayError struct { Expected int Actual int } // Error implements error. func (e InvalidArrayError) Error() string { return fmt.Sprintf( "invalid unmarshalling: expected array of length %d to match Go array but got JS array of length %d", e.Expected, e.Actual, ) } // Decoder is an interface which manually decodes js.Value on its own. // It overrides in FromJSValue. type Decoder interface { FromJSValue(js.Value) error } // FromJSValue converts a given js.Value to the Go equivalent. // The new value of 'out' is undefined if FromJSValue returns an error. // // When a JS function is unmarshalled into a Go function with only one return value, the returned JS value is casted // into the type of the return value. If the conversion fails, the function call panics. // // When a JS function is unmarshalled into a Go function with two return values, the second one being error, the // conversion error is returned instead. func FromJSValue(x js.Value, out interface{}) error { v := reflect.ValueOf(out) if v.Kind() != reflect.Ptr || v.IsNil() { return &InvalidFromJSValueError{reflect.TypeOf(v)} } return decodeValue(x, v.Elem()) } // decodeValue decodes the provided js.Value into the provided reflect.Value. func decodeValue(x js.Value, v reflect.Value) error { // If we have undefined or null, we need to be able to set to the pointer itself. // All code beyond this point are pointer-unaware so we handle undefined or null first. if x.Type() == js.TypeUndefined || x.Type() == js.TypeNull { return decodeNothing(v) } // Implementations of Decoder are probably on pointer so do it before pointer code. if d, ok := v.Addr().Interface().(Decoder); ok { return d.FromJSValue(x) } // Make sure everything is initialized and indirect it. // This prevents other decode functions from having to handle pointers. if v.Kind() == reflect.Ptr { initializePointerIfNil(v) v = reflect.Indirect(v) } if v.Kind() == reflect.Interface && v.NumMethod() == 0 { // It's a interface{} so we just create the easiest Go representation we can in createInterface. res := createInterface(x) if res != nil { v.Set(reflect.ValueOf(res)) } return nil } // Directly set v if it's a js.Value. if _, ok := v.Interface().(js.Value); ok { v.Set(reflect.ValueOf(x)) return nil } // Go the reflection route. switch x.Type() { case js.TypeBoolean: return decodeBoolean(x, v) case js.TypeNumber: return decodeNumber(x, v) case js.TypeString: return decodeString(x, v) case js.TypeSymbol: return decodeSymbol(x, v) case js.TypeObject: if isArray(x) { return decodeArray(x, v) } return decodeObject(x, v) case js.TypeFunction: return decodeFunction(x, v) default: panic("unknown JS type: " + x.Type().String()) } } // decodeNothing decodes an undefined or a null into the provided reflect.Value. func decodeNothing(v reflect.Value) error { if v.Kind() != reflect.Ptr { return InvalidTypeError{js.TypeNull, v.Type()} } v.Set(reflect.ValueOf(nil)) return nil } // decodeBoolean decodes a bool into the provided reflect.Value. func decodeBoolean(x js.Value, v reflect.Value) error { if v.Kind() != reflect.Bool { return InvalidTypeError{js.TypeBoolean, v.Type()} } v.SetBool(x.Bool()) return nil } // decodeNumber decodes a JS number into the provided reflect.Value, truncating as necessary. func decodeNumber(x js.Value, v reflect.Value) error { switch v.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: v.SetInt(int64(x.Float())) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: v.SetUint(uint64(x.Float())) case reflect.Float32, reflect.Float64: v.SetFloat(x.Float()) default: return InvalidTypeError{js.TypeNumber, v.Type()} } return nil } // decodeString decodes a JS string into the provided reflect.Value. func decodeString(x js.Value, v reflect.Value) error { if v.Kind() != reflect.String { return InvalidTypeError{js.TypeString, v.Type()} } v.SetString(x.String()) return nil } // decodeSymbol decodes a JS symbol into the provided reflect.Value. func decodeSymbol(x js.Value, v reflect.Value) error { // TODO Decode it into a symbol type. return InvalidTypeError{js.TypeSymbol, v.Type()} } // decodeArray decodes a JS array into the provided reflect.Value. func decodeArray(x js.Value, v reflect.Value) error { jsLen := x.Length() switch v.Kind() { case reflect.Array: if jsLen != v.Len() { return InvalidArrayError{v.Len(), jsLen} } case reflect.Slice: newSlice := reflect.MakeSlice(v.Type(), jsLen, jsLen) v.Set(newSlice) default: return InvalidTypeError{js.TypeObject, v.Type()} } for i := 0; i < jsLen; i++ { err := FromJSValue(x.Index(i), v.Index(i).Addr().Interface()) if err != nil { return err } } return nil } // decodeObject decodes a JS object into the provided reflect.Value. func decodeObject(x js.Value, v reflect.Value) error { switch v.Kind() { case reflect.Struct: return decodeObjectIntoStruct(x, v) case reflect.Map: return decodeObjectIntoMap(x, v) default: return InvalidTypeError{js.TypeObject, v.Type()} } } // decodeObject decodes a JS object into the provided reflect.Value struct. func decodeObjectIntoStruct(x js.Value, v reflect.Value) error { for i := 0; i < v.Type().NumField(); i++ { fieldType := v.Type().Field(i) if fieldType.PkgPath != "" { continue } name := fieldType.Name tagName, tagOK := fieldType.Tag.Lookup("wasm") if tagOK { name = tagName } err := decodeValue(x.Get(name), v.Field(i)) if err != nil { if tagOK { return fmt.Errorf("in field %s (JS %s): %w", fieldType.Name, tagName, err) } return fmt.Errorf("in field %s: %w", fieldType.Name, err) } } return nil } func decodeObjectIntoMap(x js.Value, v reflect.Value) error { mapType := v.Type() keyType := mapType.Key() valType := mapType.Elem() switch keyType.Kind() { case reflect.String: case reflect.Interface: if keyType.NumMethod() != 0 { return InvalidTypeError{js.TypeObject, mapType} } default: return InvalidTypeError{js.TypeObject, mapType} } // TODO: Use Object API obj, err := Global().Get("Object") if err != nil { panic("Object not found") } var keys []string err = FromJSValue(obj.Call("keys", x), &keys) if err != nil { panic("Object.keys returned non-string-array.") } for _, k := range keys { valuePtr := reflect.New(valType).Interface() err := FromJSValue(x.Get(k), valuePtr) if err != nil { return err } v.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(valuePtr).Elem()) } return nil } // decodeFunction decodes a JS function into the provided reflect.Value. func decodeFunction(x js.Value, v reflect.Value) error { funcType := v.Type() outCount := funcType.NumOut() switch outCount { case 0, 1: case 2: if funcType.Out(1) != errorType { return ErrMultipleReturnValue } default: return ErrMultipleReturnValue } v.Set(reflect.MakeFunc(funcType, func(args []reflect.Value) []reflect.Value { argsJS := make([]interface{}, 0, len(args)) for _, v := range args { argsJS = append(argsJS, ToJSValue(v.Interface())) } jsReturn := x.Invoke(argsJS...) if outCount == 0 { return []reflect.Value{} } returnPtr := reflect.New(funcType.Out(0)).Interface() err := FromJSValue(jsReturn, returnPtr) returnVal := reflect.ValueOf(returnPtr).Elem() if err != nil { if outCount == 1 { panic("error decoding JS return value: " + err.Error()) } return []reflect.Value{returnVal, reflect.ValueOf(err)} } switch outCount { case 1: return []reflect.Value{returnVal} case 2: return []reflect.Value{returnVal, reflect.ValueOf(nil)} default: panic("unexpected amount of return values") } })) return nil } // createInterface creates a representation of the provided js.Value. func createInterface(x js.Value) interface{} { switch x.Type() { case js.TypeUndefined, js.TypeNull: return nil case js.TypeBoolean: return x.Bool() case js.TypeNumber: return x.Float() case js.TypeString: return x.String() case js.TypeSymbol: // We can't convert it to a Go value in a meaningful way. return x case js.TypeObject: if isArray(x) { return createArray(x) } return createObject(x) case js.TypeFunction: var a func(...interface{}) (interface{}, error) err := FromJSValue(x, &a) if err != nil { panic("error creating function: " + err.Error()) } return a default: panic("unknown JS type: " + x.Type().String()) } } // createArray creates a slice of interface representing the js.Value. func createArray(x js.Value) interface{} { result := make([]interface{}, x.Length()) for i := range result { result[i] = createInterface(x.Index(i)) } return result } // createObject creates a representation of the provided JS object. func createObject(x js.Value) interface{} { // TODO: Use Object API obj, err := Global().Get("Object") if err != nil { panic("Object not found") } var keys []string err = FromJSValue(obj.Call("keys", x), &keys) if err != nil { panic("Object.keys returned non-string-array.") } result := make(map[string]interface{}, len(keys)) for _, v := range keys { result[v] = createInterface(x.Get(v)) } return result } // isArray calls the JS function Array.isArray to check if the provided js.Value is an array. func isArray(x js.Value) bool { arr, err := Global().Get("Array") if err != nil { panic("Array not found") } return arr.Call("isArray", x).Bool() } // initializePointerIfNil checks if the pointer is nil and initializes it as necessary. func initializePointerIfNil(v reflect.Value) { if v.Kind() != reflect.Ptr { return } if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } initializePointerIfNil(v.Elem()) }