Skip to content

Commit

Permalink
JSON parse and stringify API (#70)
Browse files Browse the repository at this point in the history
* JSON parse and stringify API

* move to functions rather than ctx methods to align closer to C++ API

* value implement json.Marshaler interface

* test parse error cases

* can;t use errors.As in Go 12.x
  • Loading branch information
rogchap authored Feb 3, 2021
1 parent 9005132 commit 072331f
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 10 deletions.
38 changes: 38 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package v8go

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

// JSONParse tries to parse the string and returns it as *Value if successful.
// Any JS errors will be returned as `JSError`.
func JSONParse(ctx *Context, str string) (*Value, error) {
if ctx == nil {
return nil, errors.New("v8go: Context is required")
}
cstr := C.CString(str)
defer C.free(unsafe.Pointer(cstr))

rtn := C.JSONParse(ctx.ptr, cstr)
return getValue(ctx, rtn), getError(rtn)
}

// 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 {
return "", errors.New("v8go: Value is required")
}
// If a nil context is passed we'll use the context/isolate that created the value.
var ctxPtr C.ContextPtr
if ctx != nil {
ctxPtr = ctx.ptr
}

str := C.JSONStringify(ctxPtr, val.ptr)
defer C.free(unsafe.Pointer(str))
return C.GoString(str), nil
}
47 changes: 47 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package v8go_test

import (
"fmt"
"testing"

"rogchap.com/v8go"
)

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

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

if _, ok := err.(*v8go.JSError); !ok {
t.Errorf("expected error to be of type JSError, got: %T", err)
}

}

func ExampleJSONParse() {
ctx, _ := v8go.NewContext()
val, _ := v8go.JSONParse(ctx, `{"foo": "bar"}`)
fmt.Println(val)
// Output:
// [object Object]
}

func ExampleJSONStringify() {
ctx, _ := v8go.NewContext()
val, _ := v8go.JSONParse(ctx, `{
"a": 1,
"b": "foo"
}`)
jsonStr, _ := v8go.JSONStringify(ctx, val)
fmt.Println(jsonStr)
// Output:
// {"a":1,"b":"foo"}
}
3 changes: 3 additions & 0 deletions object_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestObjectTemplate(t *testing.T) {
val, _ := v8go.NewValue(iso, "bar")
objVal, _ := v8go.NewObjectTemplate(iso)
bigbigint, _ := new(big.Int).SetString("36893488147419099136", 10) // larger than a single word size (64bit)
bigbignegint, _ := new(big.Int).SetString("-36893488147419099136", 10)

tests := [...]struct {
name string
Expand All @@ -40,7 +41,9 @@ func TestObjectTemplate(t *testing.T) {
{"u64", uint64(1)},
{"float64", float64(1)},
{"bigint", big.NewInt(1)},
{"biguint", new(big.Int).SetUint64(1 << 63)},
{"bigbigint", bigbigint},
{"bigbignegint", bigbignegint},
{"bool", true},
{"val", val},
{"obj", objVal},
Expand Down
79 changes: 69 additions & 10 deletions v8go.cc
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ TemplatePtr NewFunctionTemplate(IsolatePtr iso_ptr, int callback_ref) {

/********** Context **********/

#define LOCAL_CONTEXT(ctx_ptr) \
m_ctx* ctx = static_cast<m_ctx*>(ctx_ptr); \
Isolate* iso = ctx->iso; \
Locker locker(iso); \
Isolate::Scope isolate_scope(iso); \
HandleScope handle_scope(iso); \
TryCatch try_catch(iso); \
Local<Context> local_ctx = ctx->ptr.Get(iso); \
Context::Scope context_scope(local_ctx);

ContextPtr NewContext(IsolatePtr iso_ptr,
TemplatePtr global_template_ptr,
int ref) {
Expand Down Expand Up @@ -316,15 +326,7 @@ void ContextFree(ContextPtr ptr) {
}

RtnValue RunScript(ContextPtr ctx_ptr, const char* source, const char* origin) {
m_ctx* ctx = static_cast<m_ctx*>(ctx_ptr);
Isolate* iso = ctx->iso;
Locker locker(iso);
Isolate::Scope isolate_scope(iso);
HandleScope handle_scope(iso);
TryCatch try_catch(iso);

Local<Context> local_ctx = ctx->ptr.Get(iso);
Context::Scope context_scope(local_ctx);
LOCAL_CONTEXT(ctx_ptr);

Local<String> src =
String::NewFromUtf8(iso, source, NewStringType::kNormal).ToLocalChecked();
Expand All @@ -339,7 +341,27 @@ RtnValue RunScript(ContextPtr ctx_ptr, const char* source, const char* origin) {
rtn.error = ExceptionError(try_catch, iso, local_ctx);
return rtn;
}
MaybeLocal<v8::Value> result = script.ToLocalChecked()->Run(local_ctx);
MaybeLocal<Value> result = script.ToLocalChecked()->Run(local_ctx);
if (result.IsEmpty()) {
rtn.error = ExceptionError(try_catch, iso, local_ctx);
return rtn;
}
m_value* val = new m_value;
val->iso = iso;
val->ctx.Reset(iso, local_ctx);
val->ptr.Reset(iso, Persistent<Value>(iso, result.ToLocalChecked()));

rtn.value = static_cast<ValuePtr>(val);
return rtn;
}

RtnValue JSONParse(ContextPtr ctx_ptr, const char* str) {
LOCAL_CONTEXT(ctx_ptr);
RtnValue rtn = {nullptr, nullptr};

MaybeLocal<Value> result = JSON::Parse(
local_ctx,
String::NewFromUtf8(iso, str, NewStringType::kNormal).ToLocalChecked());
if (result.IsEmpty()) {
rtn.error = ExceptionError(try_catch, iso, local_ctx);
return rtn;
Expand All @@ -353,6 +375,43 @@ RtnValue RunScript(ContextPtr ctx_ptr, const char* source, const char* origin) {
return rtn;
}

const char* JSONStringify(ContextPtr ctx_ptr, ValuePtr val_ptr) {
Isolate* iso;
Local<Context> local_ctx;

m_value* val = static_cast<m_value*>(val_ptr);
m_ctx* ctx = static_cast<m_ctx*>(ctx_ptr);

if (ctx != nullptr) {
iso = ctx->iso;
} else {
iso = val->iso;
}

Locker locker(iso);
Isolate::Scope isolate_scope(iso);
HandleScope handle_scope(iso);

if (ctx != nullptr) {
local_ctx = ctx->ptr.Get(iso);
} else {
local_ctx = val->ctx.Get(iso);
if (local_ctx.IsEmpty()) {
m_ctx* ctx = static_cast<m_ctx*>(iso->GetData(0));
local_ctx = ctx->ptr.Get(iso);
}
}

Context::Scope context_scope(local_ctx);

MaybeLocal<String> str = JSON::Stringify(local_ctx, val->ptr.Get(iso));
if (str.IsEmpty()) {
return nullptr;
}
String::Utf8Value json(iso, str.ToLocalChecked());
return CopyString(json);
}

/********** Value **********/

#define LOCAL_VALUE(ptr) \
Expand Down
2 changes: 2 additions & 0 deletions v8go.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ extern void ContextFree(ContextPtr ptr);
extern RtnValue RunScript(ContextPtr ctx_ptr,
const char* source,
const char* origin);
extern RtnValue JSONParse(ContextPtr ctx_ptr, const char* str);
const char* JSONStringify(ContextPtr ctx_ptr, ValuePtr val_ptr);

extern void TemplateFree(TemplatePtr ptr);
extern void TemplateSetValue(TemplatePtr ptr,
Expand Down
9 changes: 9 additions & 0 deletions value.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,12 @@ func (v *Value) finalizer() {
v.ptr = nil
runtime.SetFinalizer(v, nil)
}

// MarshalJSON implements the json.Marshaler interface.
func (v *Value) MarshalJSON() ([]byte, error) {
jsonStr, err := JSONStringify(nil, v)
if err != nil {
return nil, err
}
return []byte(jsonStr), nil
}
57 changes: 57 additions & 0 deletions value_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v8go_test

import (
"bytes"
"fmt"
"math"
"math/big"
Expand Down Expand Up @@ -462,3 +463,59 @@ func TestValueIsXXX(t *testing.T) {
})
}
}

func TestValueMarshalJSON(t *testing.T) {
t.Parallel()
iso, _ := v8go.NewIsolate()

tests := [...]struct {
name string
val func() *v8go.Value
expected []byte
}{
{
"primitive",
func() *v8go.Value {
val, _ := v8go.NewValue(iso, int32(0))
return val
},
[]byte("0"),
},
{
"object",
func() *v8go.Value {
ctx, _ := v8go.NewContext(iso)
val, _ := ctx.RunScript("let foo = {a:1, b:2}; foo", "test.js")
return val
},
[]byte(`{"a":1,"b":2}`),
},
{
"objectFunc",
func() *v8go.Value {
ctx, _ := v8go.NewContext(iso)
val, _ := ctx.RunScript("let foo = {a:1, b:()=>{}}; foo", "test.js")
return val
},
[]byte(`{"a":1}`),
},
{
"nil",
func() *v8go.Value { return nil },
[]byte(""),
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
val := tt.val()
json, _ := val.MarshalJSON()
if !bytes.Equal(json, tt.expected) {
t.Errorf("unexpected JSON value: %s", string(json))
}

})
}
}

0 comments on commit 072331f

Please sign in to comment.