value.go raw

   1  //go:build js && wasm
   2  
   3  package safejs
   4  
   5  import (
   6  	"fmt"
   7  	"syscall/js"
   8  
   9  	"github.com/hack-pad/safejs/internal/catch"
  10  )
  11  
  12  // Value is a safer version of js.Value. Any panic returns an error instead.
  13  type Value struct {
  14  	jsValue js.Value
  15  }
  16  
  17  // Safe wraps a js.Value into a safejs.Value.
  18  // Ideal for use in libraries where exposed types must match the standard library.
  19  func Safe(value js.Value) Value {
  20  	return Value{
  21  		jsValue: value,
  22  	}
  23  }
  24  
  25  // Unsafe unwraps a safejs.Value back into its js.Value.
  26  // Ideal for use in libraries where exposed types must match the standard library.
  27  func Unsafe(value Value) js.Value {
  28  	return value.jsValue
  29  }
  30  
  31  // Null returns the JavaScript value of "null".
  32  func Null() Value {
  33  	return Safe(js.Null())
  34  }
  35  
  36  // Undefined returns the JavaScript value of "undefined".
  37  func Undefined() Value {
  38  	return Safe(js.Undefined())
  39  }
  40  
  41  func toJSValue(jsValue any) any {
  42  	switch value := jsValue.(type) {
  43  	case Value:
  44  		return value.jsValue
  45  	case Func:
  46  		return value.fn
  47  	case Error:
  48  		return value.err
  49  	case map[string]any:
  50  		newValue := make(map[string]any)
  51  		for mapKey, mapValue := range value {
  52  			newValue[mapKey] = toJSValue(mapValue)
  53  		}
  54  		return newValue
  55  	case []any:
  56  		newValue := make([]any, len(value))
  57  		for i, arg := range value {
  58  			newValue[i] = toJSValue(arg)
  59  		}
  60  		return newValue
  61  	default:
  62  		return jsValue
  63  	}
  64  }
  65  
  66  func toJSValues(args []any) []any {
  67  	return toJSValue(args).([]any)
  68  }
  69  
  70  func toValues(args []js.Value) []Value {
  71  	newArgs := make([]Value, len(args))
  72  	for i, arg := range args {
  73  		newArgs[i] = Safe(arg)
  74  	}
  75  	return newArgs
  76  }
  77  
  78  // ValueOf returns value as a JavaScript value. See [js.ValueOf] for details.
  79  func ValueOf(value any) (Value, error) {
  80  	jsValue, err := catch.Try(func() js.Value {
  81  		return js.ValueOf(value)
  82  	})
  83  	return Safe(jsValue), err
  84  }
  85  
  86  // Bool attempts to convert this value into a boolean, otherwise returns an error.
  87  func (v Value) Bool() (bool, error) {
  88  	return catch.Try(v.jsValue.Bool)
  89  }
  90  
  91  // Call does a JavaScript call to the method m of value v with the given arguments.
  92  // The arguments are mapped to JavaScript values according to the ValueOf function.
  93  // Returns an error if v has no method m, the arguments failed to map to JavaScript values, or the function throws an error.
  94  func (v Value) Call(m string, args ...any) (Value, error) {
  95  	args = toJSValues(args)
  96  	return catch.Try(func() Value {
  97  		return Safe(v.jsValue.Call(m, args...))
  98  	})
  99  }
 100  
 101  // Delete deletes the JavaScript property p of value v. Returns an error if v is not a JavaScript object.
 102  func (v Value) Delete(p string) error {
 103  	return catch.TrySideEffect(func() {
 104  		v.jsValue.Delete(p)
 105  	})
 106  }
 107  
 108  // Equal reports whether v and w are equal according to JavaScript's === operator.
 109  func (v Value) Equal(w Value) bool {
 110  	return v.jsValue.Equal(w.jsValue)
 111  }
 112  
 113  // Float returns the value v as a float64. Returns an error if v is not a JavaScript number.
 114  func (v Value) Float() (float64, error) {
 115  	return catch.Try(v.jsValue.Float)
 116  }
 117  
 118  // Get returns the JavaScript property p of value v. Returns an error if v is not a JavaScript object.
 119  func (v Value) Get(p string) (Value, error) {
 120  	return catch.Try(func() Value {
 121  		return Safe(v.jsValue.Get(p))
 122  	})
 123  }
 124  
 125  // Index returns JavaScript index i of value v. Returns an error if v is not a JavaScript object.
 126  func (v Value) Index(i int) (Value, error) {
 127  	return catch.Try(func() Value {
 128  		return Safe(v.jsValue.Index(i))
 129  	})
 130  }
 131  
 132  // InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator.
 133  // Returns an error if v is not a constructable type.
 134  func (v Value) InstanceOf(t Value) (bool, error) {
 135  	// Type failures in JS throw "TypeError: Right-hand side of 'instanceof' is not an object"
 136  	// so catch those cases here.
 137  	//
 138  	// A valid type is a function with a field "prototype" which is an object.
 139  	if t.Type() != TypeFunction {
 140  		return false, fmt.Errorf("invalid type for instanceof: %v", t.Type())
 141  	}
 142  	prototype, err := t.Get("prototype")
 143  	if err != nil {
 144  		return false, fmt.Errorf("invalid constructor type for instanceof: %v", err)
 145  	} else if prototype.Type() != TypeObject {
 146  		return false, fmt.Errorf("invalid constructor type for instanceof: %v", prototype.Type())
 147  	}
 148  	return catch.Try(func() bool {
 149  		return v.jsValue.InstanceOf(t.jsValue)
 150  	})
 151  }
 152  
 153  // Int returns the value v truncated to an int. Returns an error if v is not a JavaScript number.
 154  func (v Value) Int() (int, error) {
 155  	return catch.Try(v.jsValue.Int)
 156  }
 157  
 158  // Invoke does a JavaScript call of the value v with the given arguments.
 159  // The arguments get mapped to JavaScript values according to the ValueOf function.
 160  // Returns an error if v is not a JavaScript function, the arguments failed to map to JavaScript values, or the function throws an error.
 161  func (v Value) Invoke(args ...any) (Value, error) {
 162  	args = toJSValues(args)
 163  	return catch.Try(func() Value {
 164  		return Safe(v.jsValue.Invoke(args...))
 165  	})
 166  }
 167  
 168  // IsNaN reports whether v is the JavaScript value "NaN".
 169  func (v Value) IsNaN() bool {
 170  	return v.jsValue.IsNaN()
 171  }
 172  
 173  // IsNull reports whether v is the JavaScript value "null".
 174  func (v Value) IsNull() bool {
 175  	return v.jsValue.IsNull()
 176  }
 177  
 178  // IsUndefined reports whether v is the JavaScript value "undefined".
 179  func (v Value) IsUndefined() bool {
 180  	return v.jsValue.IsUndefined()
 181  }
 182  
 183  // Length returns the JavaScript property "length" of v.
 184  // Returns an error if v is not a JavaScript object.
 185  func (v Value) Length() (int, error) {
 186  	return catch.Try(v.jsValue.Length)
 187  }
 188  
 189  // New uses JavaScript's "new" operator with value v as constructor and the given arguments.
 190  // The arguments get mapped to JavaScript values according to the ValueOf function.
 191  // Returns an error if v is not a JavaScript function, the arguments failed to map to JavaScript values, or the constructor throws an error.
 192  func (v Value) New(args ...any) (Value, error) {
 193  	args = toJSValues(args)
 194  	return catch.Try(func() Value {
 195  		return Safe(v.jsValue.New(args...))
 196  	})
 197  }
 198  
 199  // Set sets the JavaScript property p of value v to ValueOf(x).
 200  // Returns an error if v is not a JavaScript object or x failed to map to a JavaScript value.
 201  func (v Value) Set(p string, x any) error {
 202  	x = toJSValue(x)
 203  	return catch.TrySideEffect(func() {
 204  		v.jsValue.Set(p, x)
 205  	})
 206  }
 207  
 208  // SetIndex sets the JavaScript index i of value v to ValueOf(x).
 209  // Returns an error if if v is not a JavaScript object or x failed to map to a JavaScript value.
 210  func (v Value) SetIndex(i int, x any) error {
 211  	x = toJSValue(x)
 212  	return catch.TrySideEffect(func() {
 213  		v.jsValue.SetIndex(i, x)
 214  	})
 215  }
 216  
 217  // String returns the value v as a string.
 218  // Unlike the other getters, String() does not return an error if v's Type is not TypeString.
 219  // Instead, it returns a string of the form "<T>" or "<T: V>" where T is v's type and V is a string representation of v's value.
 220  //
 221  // Returns an error if v is an invalid type or the string failed to load from the JavaScript runtime.
 222  //
 223  // NOTE: [syscall/js] takes the stance that String is a special case due to Go's String method convention and avoids panicking.
 224  // However, js.String() can still fail in other ways so an error is returned anyway.
 225  func (v Value) String() (string, error) {
 226  	return catch.Try(v.jsValue.String)
 227  }
 228  
 229  // Truthy returns the JavaScript "truthiness" of the value v.
 230  // In JavaScript, false, 0, "", null, undefined, and NaN are "falsy", and everything else is "truthy".
 231  // See https://developer.mozilla.org/en-US/docs/Glossary/Truthy.
 232  //
 233  // Returns an error if v's type is invalid or if the value fails to load from the JavaScript runtime.
 234  func (v Value) Truthy() (bool, error) {
 235  	return catch.Try(v.jsValue.Truthy)
 236  }
 237  
 238  // Type returns the JavaScript type of the value v.
 239  // It is similar to JavaScript's typeof operator, except it returns TypeNull instead of TypeObject for null.
 240  func (v Value) Type() Type {
 241  	return Type(v.jsValue.Type())
 242  }
 243