-
Notifications
You must be signed in to change notification settings - Fork 570
[WIP] js: tighten up internalize and externalize #618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,60 @@ | ||
// Package js provides functions for interacting with native JavaScript APIs. Calls to these functions are treated specially by GopherJS and translated directly to their corresponding JavaScript syntax. | ||
// Package js provides functions for interacting with native JavaScript APIs. | ||
// Calls to these functions are treated specially by GopherJS and translated | ||
// directly to their corresponding JavaScript syntax. | ||
// | ||
// Use MakeWrapper to expose methods to JavaScript. When passing values directly, the following type conversions are performed: | ||
// Use MakeWrapper to expose methods to JavaScript. | ||
// | ||
// | Go type | JavaScript type | Conversions back to interface{} | | ||
// | --------------------- | --------------------- | ------------------------------- | | ||
// | bool | Boolean | bool | | ||
// | integers and floats | Number | float64 | | ||
// | string | String | string | | ||
// | []int8 | Int8Array | []int8 | | ||
// | []int16 | Int16Array | []int16 | | ||
// | []int32, []int | Int32Array | []int | | ||
// | []uint8 | Uint8Array | []uint8 | | ||
// | []uint16 | Uint16Array | []uint16 | | ||
// | []uint32, []uint | Uint32Array | []uint | | ||
// | []float32 | Float32Array | []float32 | | ||
// | []float64 | Float64Array | []float64 | | ||
// | all other slices | Array | []interface{} | | ||
// | arrays | see slice type | see slice type | | ||
// | functions | Function | func(...interface{}) *js.Object | | ||
// | time.Time | Date | time.Time | | ||
// | - | instanceof Node | *js.Object | | ||
// | maps, structs | instanceof Object | map[string]interface{} | | ||
// Internalization | ||
// | ||
// Additionally, for a struct containing a *js.Object field, only the content of the field will be passed to JavaScript and vice versa. | ||
// When values pass from Javascript to Go, a process known as internalization, | ||
// the following conversion table is applied: | ||
// | ||
// |----------------+---------------+-------------------------+--------| | ||
// | Go target type | Translation | Javascript source value | Result | | ||
// |----------------+---------------+-------------------------+--------| | ||
// | string | UTF16 -> UTF8 | null | "" | | ||
// | | | undefined | "" | | ||
// | | | "" | "" | | ||
// | | | new String("") | "" | | ||
// | | | "ok" † | "ok" | | ||
// | | | new String("ok") † | "ok" | | ||
// |----------------+---------------+-------------------------+--------| | ||
// | bool | none | null | false | | ||
// | | | undefined | false | | ||
// | | | false † | false | | ||
// | | | new Boolean(false) † | false | | ||
// |----------------+---------------+-------------------------+--------| | ||
// | ||
// Any source values not listed in this table cause a runtime panic for a given | ||
// target type if a conversion is attempted, e.g. a Javascript number value | ||
// being assigned to a string type Go variable. | ||
// | ||
// Source values annotated with † are generally applicable to all valid | ||
// values of the target type. e.g. for target type string, "ok" represents | ||
// all valid string primitive values. | ||
// | ||
// Externalization | ||
// | ||
// When values pass from Go to Javascript, a process known as externalization, | ||
// the following conversion table is applied: | ||
// | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Table missing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See next line. |
||
// |----------------+---------------+-----------------+--------+---------+-------------| | ||
// | Go source type | Translation | Go source value | Result | typeof | constructor | | ||
// |----------------+---------------+-----------------+--------+---------+-------------| | ||
// | string | UTF8 -> UTF16 | "" | "" | string | String | | ||
// | | | "ok" † | "ok" | | | | ||
// |----------------+---------------+-----------------+--------+---------+-------------| | ||
// | bool | none | false | false | boolean | Boolean | | ||
// | | | true | true | | | | ||
// |----------------+---------------+-----------------+--------+---------+-------------| | ||
// | ||
// Source values annotated with † are generally applicable to all valid | ||
// values of the target type. e.g. for target type string, "ok" represents | ||
// all valid string values. | ||
// | ||
// Special struct types | ||
// | ||
// To follow.... | ||
package js | ||
|
||
// Object is a container for a native JavaScript object. Calls to its methods are treated specially by GopherJS and translated directly to their JavaScript syntax. A nil pointer to Object is equal to JavaScript's "null". Object can not be used as a map key. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -467,7 +467,7 @@ func TestCallWithNull(t *testing.T) { | |
func TestReflection(t *testing.T) { | ||
o := js.Global.Call("eval", "({ answer: 42 })") | ||
if reflect.ValueOf(o).Interface().(*js.Object) != o { | ||
t.Fail() | ||
t.Fatal() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it not possible to include a helpful error message in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be honest I was being a little lazy here in just flipping the existing I'm sure we could add helpful messages to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's usually easier to do these minor things as you're working in a relevant context than later. But this is completely up to you, and not critical for getting the PR into a mergeable state. |
||
} | ||
|
||
type S struct { | ||
|
@@ -477,18 +477,19 @@ func TestReflection(t *testing.T) { | |
|
||
v := reflect.ValueOf(&s).Elem() | ||
if v.Field(0).Interface().(*js.Object).Get("answer").Int() != 42 { | ||
t.Fail() | ||
t.Fatal() | ||
} | ||
if v.Field(0).MethodByName("Get").Call([]reflect.Value{reflect.ValueOf("answer")})[0].Interface().(*js.Object).Int() != 42 { | ||
t.Fail() | ||
t.Fatal() | ||
} | ||
v.Field(0).Set(reflect.ValueOf(js.Global.Call("eval", "({ answer: 100 })"))) | ||
if s.Field.Get("answer").Int() != 100 { | ||
t.Fail() | ||
t.Fatal() | ||
} | ||
|
||
if fmt.Sprintf("%+v", s) != "{Field:[object Object]}" { | ||
t.Fail() | ||
expFmt := "{Field:[object Object]}" | ||
if v := fmt.Sprintf("%+v", s); v != expFmt { | ||
t.Fatalf("fmt out was: %q; expected %q", v, expFmt) | ||
} | ||
} | ||
|
||
|
@@ -620,3 +621,207 @@ func TestStructWithNonIdentifierJSTag(t *testing.T) { | |
t.Errorf("value via js.Object.Get gave %q, want %q", got, want) | ||
} | ||
} | ||
|
||
// Internalize is used as a helper type to test $internalize. A struct is used | ||
// in order that all of the Go types can be verified (otherwise we are limited | ||
// to the methods on *Object). Where methods on *Object exist they too will be | ||
// tested | ||
type Internalize struct { | ||
*js.Object | ||
|
||
string string `js:"string"` | ||
bool bool `js:"bool"` | ||
} | ||
|
||
func TestInternalizeString(t *testing.T) { | ||
fieldName := "string" | ||
zero := "" | ||
|
||
s := &Internalize{Object: js.Global.Get("Object").New()} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stylistic note: I wonder if a table-based test would make sense here? It may make the tests more readable, reduce the need for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You raise a good point. I half considered this myself, but postponed giving it much thought until we've confirmed we're actually testing the right stuff... at which point extracting to table tests should be relatively easy. The little thought I did give it however amount to the following: I'm not convinced we can actually use table tests. We need to address specific fields of the Therefore the one approach I have considered fairly seriously is code generating the tests. Because they do follow a formula that's for sure. But as I said, I haven't given this much time/though so would very much welcome any suggestions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @flimzy just pushed up a code-generated version of the tests which seems to work well. Let me know what you think. |
||
|
||
// ************* | ||
// undefined | ||
// ************* | ||
s.Object.Set(fieldName, jsundefined()) | ||
|
||
// via struct field | ||
if v := s.string; v != zero { | ||
t.Fatalf("expected string field to be %q, got %q", zero, v) | ||
} | ||
|
||
// via *js.Object.String() | ||
if v := s.Object.Get(fieldName).String(); v != zero { | ||
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// null | ||
// ************* | ||
s.Object.Set(fieldName, jsnull()) | ||
|
||
// via struct field | ||
if v := s.string; v != zero { | ||
t.Fatalf("expected string field to be %q, got %q", zero, v) | ||
} | ||
|
||
// via *js.Object.String() | ||
if v := s.Object.Get(fieldName).String(); v != zero { | ||
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// valid primitive string | ||
// ************* | ||
exp := "ok" | ||
s.Object.Set(fieldName, jsval(`"`+exp+`"`)) | ||
|
||
// via struct field | ||
if v := s.string; v != exp { | ||
t.Fatalf("expected string field to be %q, got %q", zero, v) | ||
} | ||
|
||
// via *js.Object.String() | ||
if v := s.Object.Get(fieldName).String(); v != exp { | ||
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// valid String object | ||
// ************* | ||
s.Object.Set(fieldName, jsval(`new String("`+exp+`")`)) | ||
|
||
// via struct field | ||
if v := s.string; v != exp { | ||
t.Fatalf("expected string field to be %q, got %q", zero, v) | ||
} | ||
|
||
// via *js.Object.String() | ||
if v := s.Object.Get(fieldName).String(); v != exp { | ||
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// invalid | ||
// ************* | ||
s.Object.Set(fieldName, jsval("5")) | ||
|
||
shouldRuntimePanic(t, "runtime error: tried to internalize non-string value of type number/Number", func() { | ||
_ = s.string | ||
}) | ||
|
||
shouldRuntimePanic(t, "runtime error: tried to internalize non-string value of type number/Number", func() { | ||
_ = s.Object.Get(fieldName).String() | ||
}) | ||
} | ||
|
||
func TestInternalizeBool(t *testing.T) { | ||
fieldName := "bool" | ||
zero := false | ||
|
||
s := &Internalize{Object: js.Global.Get("Object").New()} | ||
|
||
// ************* | ||
// undefined | ||
// ************* | ||
s.Object.Set(fieldName, jsundefined()) | ||
|
||
// via struct field | ||
if v := s.bool; v != zero { | ||
t.Fatalf("expected bool field to be %q, got %q", zero, v) | ||
} | ||
|
||
// via *js.Object.Bool() | ||
if v := s.Object.Get(fieldName).Bool(); v != zero { | ||
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// null | ||
// ************* | ||
s.Object.Set(fieldName, jsnull()) | ||
|
||
// via struct field | ||
if v := s.bool; v != zero { | ||
t.Fatalf("expected bool field to be %q, got %q", zero, v) | ||
} | ||
|
||
// via *js.Object.Bool() | ||
if v := s.Object.Get(fieldName).Bool(); v != zero { | ||
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// valid primitive bool | ||
// ************* | ||
exp := true | ||
s.Object.Set(fieldName, jsval(`true`)) | ||
|
||
// via struct field | ||
if v := s.bool; v != exp { | ||
t.Fatalf("expected bool field to be %v, got %v", exp, v) | ||
} | ||
|
||
// via *js.Object.Bool() | ||
if v := s.Object.Get(fieldName).Bool(); v != exp { | ||
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// valid Bool object | ||
// ************* | ||
s.Object.Set(fieldName, jsval(`new Boolean(true)`)) | ||
|
||
// via struct field | ||
if v := s.bool; v != exp { | ||
t.Fatalf("expected bool field to be %q, got %q", zero, v) | ||
} | ||
|
||
// via *js.Object.Bool() | ||
if v := s.Object.Get(fieldName).Bool(); v != exp { | ||
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v) | ||
} | ||
|
||
// ************* | ||
// invalid | ||
// ************* | ||
s.Object.Set(fieldName, jsval("5")) | ||
|
||
shouldRuntimePanic(t, "runtime error: tried to internalize non-bool value of type number/Number", func() { | ||
_ = s.bool | ||
}) | ||
|
||
shouldRuntimePanic(t, "runtime error: tried to internalize non-bool value of type number/Number", func() { | ||
_ = s.Object.Get(fieldName).Bool() | ||
}) | ||
} | ||
|
||
func jsval(v string) *js.Object { | ||
return js.Global.Call("eval", v) | ||
} | ||
|
||
func jsnull() *js.Object { | ||
return js.Global.Call("eval", "null") | ||
} | ||
|
||
func jsundefined() *js.Object { | ||
return js.Global.Call("eval", "undefined") | ||
} | ||
|
||
func consolelog(args ...interface{}) { | ||
js.Global.Get("console").Call("log", args...) | ||
} | ||
|
||
func shouldRuntimePanic(t *testing.T, msg string, f func()) { | ||
defer func() { | ||
err, ok := recover().(error) | ||
if !ok { | ||
t.Fatalf("expected to have had to handle panic; we didn't see a panic") | ||
} | ||
|
||
if err.Error() != msg { | ||
t.Fatalf("expected error %q, got %q", msg, err) | ||
} | ||
}() | ||
|
||
f() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth including
true
examples here, for the sake of completeness, similar to the non-zero value examples for strings?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would end up adding two lines:
true
andnew Boolean(true)
- hence why I opted for the † callout.