Skip to content

Commit

Permalink
Extend Value into the Object type (#74)
Browse files Browse the repository at this point in the history
* basic return of an Object from value

* implement object set property

* get the global object and get object instance from template

* Object Get methods and locking in the API for this PR

* fix C++ failing issues

* basic tests for the Object methods

* value tests and iso fix

* update changelog

* address comments from review
  • Loading branch information
rogchap authored Feb 8, 2021
1 parent 85aad81 commit 6305aab
Show file tree
Hide file tree
Showing 12 changed files with 441 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Create Object Templates with primitive values, including other Object Templates
- Configure Object Template as the global object of any new Context
- Function Templates with callbacks to Go
- Value to Object type, including Get/Set/Has/Delete methods
- Get Global Object from the Context
- Convert a Object Template to an instance of an Object

### Changed
- NewContext() API has been improved to handle optional global object, as well as optional Isolate
- Package error messages are now prefixed with `v8go` rather than the struct name
- Deprecated `iso.Close()` in favor of `iso.Dispose()` to keep consistancy with the C++ API
- Upgraded V8 to 8.8.278.14
- Licence BSD 3-Clause (same as V8 and Go)

## [v0.4.0] - 2021-01-14

Expand Down
14 changes: 14 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ func (c *Context) RunScript(source string, origin string) (*Value, error) {
return getValue(c, rtn), getError(rtn)
}

// Global returns the global proxy object.
// Global proxy object is a thin wrapper whose prototype points to actual
// context's global object with the properties like Object, etc. This is
// done that way for security reasons.
// Please note that changes to global proxy object prototype most probably
// would break the VM — V8 expects only global object as a prototype of
// global proxy object.
func (c *Context) Global() *Object {
valPtr := C.ContextGlobal(c.ptr)
v := &Value{valPtr, c}
runtime.SetFinalizer(v, (*Value).finalizer)
return &Object{v}
}

// Close will dispose the context and free the memory.
func (c *Context) Close() {
c.finalizer()
Expand Down
6 changes: 3 additions & 3 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ func JSONParse(ctx *Context, str string) (*Value, error) {
}

// JSONStringify tries to stringify the JSON-serializable object value and returns it as string.
func JSONStringify(ctx *Context, val *Value) (string, error) {
if val == nil {
func JSONStringify(ctx *Context, val Valuer) (string, error) {
if val == nil || val.value() == nil {
return "", errors.New("v8go: Value is required")
}
// If a nil context is passed we'll use the context/isolate that created the value.
Expand All @@ -36,7 +36,7 @@ func JSONStringify(ctx *Context, val *Value) (string, error) {
ctxPtr = ctx.ptr
}

str := C.JSONStringify(ctxPtr, val.ptr)
str := C.JSONStringify(ctxPtr, val.value().ptr)
defer C.free(unsafe.Pointer(str))
return C.GoString(str), nil
}
8 changes: 8 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ func TestJSONParse(t *testing.T) {
if _, ok := err.(*v8go.JSError); !ok {
t.Errorf("expected error to be of type JSError, got: %T", err)
}
}

func TestJSONStringify(t *testing.T) {
t.Parallel()

ctx, _ := v8go.NewContext()
if _, err := v8go.JSONStringify(ctx, nil); err == nil {
t.Error("expected error but got <nil>")
}
}

func ExampleJSONParse() {
Expand Down
103 changes: 103 additions & 0 deletions object.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package v8go

// #include <stdlib.h>
// #include "v8go.h"
import "C"
import (
"errors"
"fmt"
"math/big"
"unsafe"
)

// Object is a JavaScript object (ECMA-262, 4.3.3)
type Object struct {
*Value
}

// Set will set a property on the Object to a given value.
// Supports all value types, eg: Object, Array, Date, Set, Map etc
// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int)
// then a *Value will be created and set as the value property.
func (o *Object) Set(key string, val interface{}) error {
if len(key) == 0 {
return errors.New("v8go: You must provide a valid property key")
}
return set(o, key, 0, val)
}

// Set will set a given index on the Object to a given value.
// Supports all value types, eg: Object, Array, Date, Set, Map etc
// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int)
// then a *Value will be created and set as the value property.
func (o *Object) SetIdx(idx uint32, val interface{}) error {
return set(o, "", idx, val)
}

func set(o *Object, key string, idx uint32, val interface{}) error {
var value *Value
switch v := val.(type) {
case string, int32, uint32, int64, uint64, float64, bool, *big.Int:
// ignoring error as code cannot reach the error state as we are already
// validating the new value types in this case statement
value, _ = NewValue(o.ctx.iso, v)
case Valuer:
value = v.value()
default:
return fmt.Errorf("v8go: unsupported object property type `%T`", v)
}

if len(key) > 0 {
ckey := C.CString(key)
defer C.free(unsafe.Pointer(ckey))
C.ObjectSet(o.ptr, ckey, value.ptr)
return nil
}

C.ObjectSetIdx(o.ptr, C.uint32_t(idx), value.ptr)
return nil
}

// Get tries to get a Value for a given Object property key.
func (o *Object) Get(key string) (*Value, error) {
ckey := C.CString(key)
defer C.free(unsafe.Pointer(ckey))

rtn := C.ObjectGet(o.ptr, ckey)
return getValue(o.ctx, rtn), getError(rtn)
}

// GetIdx tries to get a Value at a give Object index.
func (o *Object) GetIdx(idx uint32) (*Value, error) {
rtn := C.ObjectGetIdx(o.ptr, C.uint32_t(idx))
return getValue(o.ctx, rtn), getError(rtn)
}

// Has calls the abstract operation HasProperty(O, P) described in ECMA-262, 7.3.10.
// Returns true, if the object has the property, either own or on the prototype chain.
func (o *Object) Has(key string) bool {
ckey := C.CString(key)
defer C.free(unsafe.Pointer(ckey))
return C.ObjectHas(o.ptr, ckey) != 0
}

// HasIdx returns true if the object has a value at the given index.
func (o *Object) HasIdx(idx uint32) bool {
return C.ObjectHasIdx(o.ptr, C.uint32_t(idx)) != 0
}

// Delete returns true if successful in deleting a named property on the object.
func (o *Object) Delete(key string) bool {
ckey := C.CString(key)
defer C.free(unsafe.Pointer(ckey))
return C.ObjectDelete(o.ptr, ckey) != 0
}

// DeleteIdx returns true if successful in deleting a value at a given index of the object.
func (o *Object) DeleteIdx(idx uint32) bool {
return C.ObjectDeleteIdx(o.ptr, C.uint32_t(idx)) != 0
}
10 changes: 10 additions & 0 deletions object_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ func NewObjectTemplate(iso *Isolate) (*ObjectTemplate, error) {
return &ObjectTemplate{tmpl}, nil
}

// NewInstance creates a new Object based on the template.
func (o *ObjectTemplate) NewInstance(ctx *Context) (*Object, error) {
if ctx == nil {
return nil, errors.New("v8go: Context cannot be <nil>")
}

valPtr := C.ObjectTemplateNewInstance(o.ptr, ctx.ptr)
return &Object{&Value{valPtr, ctx}}, nil
}

func (o *ObjectTemplate) apply(opts *contextOptions) {
opts.gTmpl = o
}
17 changes: 17 additions & 0 deletions object_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,20 @@ func TestGlobalObjectTemplate(t *testing.T) {
})
}
}

func TestObjectTemplateNewInstance(t *testing.T) {
t.Parallel()
iso, _ := v8go.NewIsolate()
tmpl, _ := v8go.NewObjectTemplate(iso)
if _, err := tmpl.NewInstance(nil); err == nil {
t.Error("expected error but got <nil>")
}

tmpl.Set("foo", "bar")
ctx, _ := v8go.NewContext(iso)
obj, _ := tmpl.NewInstance(ctx)
if foo, _ := obj.Get("foo"); foo.String() != "bar" {
t.Errorf("unexpected value for object property: %v", foo)
}

}
116 changes: 116 additions & 0 deletions object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package v8go_test

import (
"fmt"
"testing"

"rogchap.com/v8go"
)

func TestObjectSet(t *testing.T) {
t.Parallel()

ctx, _ := v8go.NewContext()
val, _ := ctx.RunScript("const foo = {}; foo", "")
obj, _ := val.AsObject()
obj.Set("bar", "baz")
baz, _ := ctx.RunScript("foo.bar", "")
if baz.String() != "baz" {
t.Errorf("unexpected value: %q", baz)
}
if err := obj.Set("", nil); err == nil {
t.Error("expected error but got <nil>")
}
if err := obj.Set("a", 0); err == nil {
t.Error("expected error but got <nil>")
}
obj.SetIdx(10, "ten")
if ten, _ := ctx.RunScript("foo[10]", ""); ten.String() != "ten" {
t.Errorf("unexpected value: %q", ten)
}
}

func TestObjectGet(t *testing.T) {
t.Parallel()

ctx, _ := v8go.NewContext()
val, _ := ctx.RunScript("const foo = { bar: 'baz'}; foo", "")
obj, _ := val.AsObject()
if bar, _ := obj.Get("bar"); bar.String() != "baz" {
t.Errorf("unexpected value: %q", bar)
}
if baz, _ := obj.Get("baz"); !baz.IsUndefined() {
t.Errorf("unexpected value: %q", baz)
}
ctx.RunScript("foo[5] = 5", "")
if five, _ := obj.GetIdx(5); five.Integer() != 5 {
t.Errorf("unexpected value: %q", five)
}
if u, _ := obj.GetIdx(55); !u.IsUndefined() {
t.Errorf("unexpected value: %q", u)
}
}

func TestObjectHas(t *testing.T) {
t.Parallel()

ctx, _ := v8go.NewContext()
val, _ := ctx.RunScript("const foo = {a: 1, '2': 2}; foo", "")
obj, _ := val.AsObject()
if !obj.Has("a") {
t.Error("expected true, got false")
}
if obj.Has("c") {
t.Error("expected false, got true")
}
if !obj.HasIdx(2) {
t.Error("expected true, got false")
}
if obj.HasIdx(1) {
t.Error("expected false, got true")
}
}

func TestObjectDelete(t *testing.T) {
t.Parallel()

ctx, _ := v8go.NewContext()
val, _ := ctx.RunScript("const foo = { bar: 'baz', '2': 2}; foo", "")
obj, _ := val.AsObject()
if !obj.Has("bar") {
t.Error("expected property to exist")
}
if !obj.Delete("bar") {
t.Error("expected delete to return true, got false")
}
if obj.Has("bar") {
t.Error("expected property to be deleted")
}
if !obj.DeleteIdx(2) {
t.Error("expected delete to return true, got false")
}

}

func ExampleObject_global() {
iso, _ := v8go.NewIsolate()
ctx, _ := v8go.NewContext(iso)
global := ctx.Global()

console, _ := v8go.NewObjectTemplate(iso)
logfn, _ := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
fmt.Println(info.Args()[0])
return nil
})
console.Set("log", logfn)
consoleObj, _ := console.NewInstance(ctx)

global.Set("console", consoleObj)
ctx.RunScript("console.log('foo')", "")
// Output:
// foo
}
Loading

0 comments on commit 6305aab

Please sign in to comment.