diff --git a/json.go b/json.go new file mode 100644 index 00000000..902ed967 --- /dev/null +++ b/json.go @@ -0,0 +1,38 @@ +package v8go + +// #include +// #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 +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 00000000..722eb066 --- /dev/null +++ b/json_test.go @@ -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 ") + } + ctx, _ := v8go.NewContext() + _, err := v8go.JSONParse(ctx, "{") + if err == nil { + t.Error("expected error but got ") + 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"} +} diff --git a/object_template_test.go b/object_template_test.go index 66e6226e..2bfe05c1 100644 --- a/object_template_test.go +++ b/object_template_test.go @@ -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 @@ -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}, diff --git a/v8go.cc b/v8go.cc index 5e3c16cd..0bf89370 100644 --- a/v8go.cc +++ b/v8go.cc @@ -273,6 +273,16 @@ TemplatePtr NewFunctionTemplate(IsolatePtr iso_ptr, int callback_ref) { /********** Context **********/ +#define LOCAL_CONTEXT(ctx_ptr) \ + m_ctx* ctx = static_cast(ctx_ptr); \ + Isolate* iso = ctx->iso; \ + Locker locker(iso); \ + Isolate::Scope isolate_scope(iso); \ + HandleScope handle_scope(iso); \ + TryCatch try_catch(iso); \ + Local local_ctx = ctx->ptr.Get(iso); \ + Context::Scope context_scope(local_ctx); + ContextPtr NewContext(IsolatePtr iso_ptr, TemplatePtr global_template_ptr, int ref) { @@ -316,15 +326,7 @@ void ContextFree(ContextPtr ptr) { } RtnValue RunScript(ContextPtr ctx_ptr, const char* source, const char* origin) { - m_ctx* ctx = static_cast(ctx_ptr); - Isolate* iso = ctx->iso; - Locker locker(iso); - Isolate::Scope isolate_scope(iso); - HandleScope handle_scope(iso); - TryCatch try_catch(iso); - - Local local_ctx = ctx->ptr.Get(iso); - Context::Scope context_scope(local_ctx); + LOCAL_CONTEXT(ctx_ptr); Local src = String::NewFromUtf8(iso, source, NewStringType::kNormal).ToLocalChecked(); @@ -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 result = script.ToLocalChecked()->Run(local_ctx); + MaybeLocal 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(iso, result.ToLocalChecked())); + + rtn.value = static_cast(val); + return rtn; +} + +RtnValue JSONParse(ContextPtr ctx_ptr, const char* str) { + LOCAL_CONTEXT(ctx_ptr); + RtnValue rtn = {nullptr, nullptr}; + + MaybeLocal 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; @@ -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 local_ctx; + + m_value* val = static_cast(val_ptr); + m_ctx* ctx = static_cast(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(iso->GetData(0)); + local_ctx = ctx->ptr.Get(iso); + } + } + + Context::Scope context_scope(local_ctx); + + MaybeLocal 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) \ diff --git a/v8go.h b/v8go.h index 77fe4033..cad91633 100644 --- a/v8go.h +++ b/v8go.h @@ -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, diff --git a/value.go b/value.go index b76018f7..8eca15a4 100644 --- a/value.go +++ b/value.go @@ -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 +} diff --git a/value_test.go b/value_test.go index 784975ed..c6363bbe 100644 --- a/value_test.go +++ b/value_test.go @@ -1,6 +1,7 @@ package v8go_test import ( + "bytes" "fmt" "math" "math/big" @@ -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)) + } + + }) + } +}