value.go 7.6 KB

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