From 09df7faf320cc4af9eba220edad163e31849c932 Mon Sep 17 00:00:00 2001 From: ngocphuongnb Date: Tue, 28 Oct 2025 18:54:24 +0700 Subject: [PATCH] refactor: rename symbols and enhance go value passthrough --- common.go | 10 +-- common_test.go | 4 +- context_test.go | 2 +- errors.go | 4 +- functojs.go | 14 ++-- functojs_test.go | 2 +- gotojs.go | 117 +++++++++++++++------------- gotojs_test.go | 18 ++--- jstogo.go | 199 +++++++++++++++++++++++++---------------------- jstogo_test.go | 57 +++++++++++--- runtime_test.go | 4 +- value.go | 2 +- 12 files changed, 246 insertions(+), 187 deletions(-) diff --git a/common.go b/common.go index 8609a5f..84c09ab 100644 --- a/common.go +++ b/common.go @@ -369,7 +369,7 @@ func (ac *JsArrayToGoConverter[T]) convertToInterface(jsArray *Array, jsLen int6 for i := range jsLen { jsElem := jsArray.Get(i) - goElem, convErr := jsValueToGo[any](ac.tracker, jsElem) + goElem, convErr := toGoValue[any](ac.tracker, jsElem) if !jsElem.IsFunction() { jsElem.Free() @@ -398,7 +398,7 @@ func (ac *JsArrayToGoConverter[T]) convertToSlice(jsArray *Array, jsLen int64) ( for i := range jsLen { jsElem := jsArray.Get(i) elemSample := reflect.New(elemType).Elem().Interface() - goElem, convErr := jsValueToGo(ac.tracker, jsElem, elemSample) + goElem, convErr := toGoValue(ac.tracker, jsElem, elemSample) if !jsElem.IsFunction() { jsElem.Free() @@ -433,7 +433,7 @@ func (ac *JsArrayToGoConverter[T]) convertToArray(jsArray *Array, jsLen int64) ( for i := range jsLen { jsElem := jsArray.Get(i) elemSample := reflect.New(elemType).Elem().Interface() - goElem, convErr := jsValueToGo(ac.tracker, jsElem, elemSample) + goElem, convErr := toGoValue(ac.tracker, jsElem, elemSample) if !jsElem.IsFunction() { jsElem.Free() @@ -593,7 +593,7 @@ func processTempValue[T any](prefix string, temp any, err error, samples ...T) ( return tempT, nil } - return v, newInvalidGoTargetErr(GetGoTypeName(sample), temp) + return v, newInvalidGoTypeErr(GetGoTypeName(sample), temp) } valueT, _ := temp.(T) @@ -769,7 +769,7 @@ func isGoStruct(goType reflect.Type) bool { // VerifyGoFunc validates that a function signature is compatible with JS conversion. func VerifyGoFunc(fnType reflect.Type, sample any) error { if fnType == nil || fnType.Kind() != reflect.Func { - return newInvalidGoTargetErr("function", sample) + return newInvalidGoTypeErr("function", sample) } // Validate that all return values are convertible to JS diff --git a/common_test.go b/common_test.go index 3be6205..bac01e6 100644 --- a/common_test.go +++ b/common_test.go @@ -536,14 +536,14 @@ func TestCreateGoBindFuncType(t *testing.T) { t.Run("non_function", func(t *testing.T) { _, err := qjs.CreateGoBindFuncType("not a function") assert.Error(t, err) - assert.Contains(t, err.Error(), "expected GO target") + assert.Contains(t, err.Error(), "expected GO type") assert.Contains(t, err.Error(), "function") }) t.Run("integer", func(t *testing.T) { _, err := qjs.CreateGoBindFuncType(42) assert.Error(t, err) - assert.Contains(t, err.Error(), "expected GO target") + assert.Contains(t, err.Error(), "expected GO type") assert.Contains(t, err.Error(), "function") }) }) diff --git a/context_test.go b/context_test.go index 55ccf3f..abe661f 100644 --- a/context_test.go +++ b/context_test.go @@ -43,7 +43,7 @@ func TestContextValueCreation(t *testing.T) { fmt.Printf("context check: is nil=%t, value=%v\n", ctx == nil, ctx) return num * 2 } - jsFuncWithContext := must(qjs.ToJSValue(ctx, goFuncWithContext)) + jsFuncWithContext := must(qjs.ToJsValue(ctx, goFuncWithContext)) defer jsFuncWithContext.Free() ctx.Global().SetPropertyStr("funcWithContext", jsFuncWithContext) diff --git a/errors.go b/errors.go index b1539bf..16b467a 100644 --- a/errors.go +++ b/errors.go @@ -108,8 +108,8 @@ func newArgConversionErr(index int, err error) error { return fmt.Errorf("cannot convert JS function argument at index %d: %w", index, err) } -func newInvalidGoTargetErr(expect string, got any) error { - return fmt.Errorf("expected GO target %s, got %T", expect, got) +func newInvalidGoTypeErr(expect string, got any) error { + return fmt.Errorf("expected GO type %s, got %T", expect, got) } func newInvalidJsInputErr(kind string, input *Value) (err error) { diff --git a/functojs.go b/functojs.go index a53cf1b..bef2bf9 100644 --- a/functojs.go +++ b/functojs.go @@ -30,7 +30,7 @@ func FuncToJS(c *Context, v any) (_ *Value, err error) { } if rtype.Kind() != reflect.Func { - return nil, newInvalidGoTargetErr("function", v) + return nil, newInvalidGoTypeErr("function", v) } if rval.IsNil() { @@ -114,7 +114,7 @@ func handlePointerArgument(jsArg *Value, argType reflect.Type) (reflect.Value, e underlyingType := argType.Elem() zeroVal := reflect.New(underlyingType).Elem() - goVal, err := JsValueToGo(jsArg, zeroVal.Interface()) + goVal, err := ToGoValue(jsArg, zeroVal.Interface()) if err != nil { return reflect.Value{}, newJsToGoErr(jsArg, err, "function param pointer to "+jsArg.Type()) } @@ -184,7 +184,7 @@ func JsArgToGo(jsArg *Value, argType reflect.Type) (reflect.Value, error) { goZeroVal := CreateNonNilSample(argType) - goVal, err := JsValueToGo(jsArg, goZeroVal) + goVal, err := ToGoValue(jsArg, goZeroVal) if err != nil { return reflect.Value{}, newJsToGoErr(jsArg, err, "function param "+jsArg.Type()) } @@ -241,7 +241,7 @@ func GoFuncResultToJs(c *Context, results []reflect.Value) (*Value, error) { // Single remaining value -> return that value if len(remaining) == 1 { - return ToJSValue(c, remaining[0].Interface()) + return ToJsValue(c, remaining[0].Interface()) } // Multiple remaining values -> return as JS array @@ -250,12 +250,12 @@ func GoFuncResultToJs(c *Context, results []reflect.Value) (*Value, error) { jsValues[i] = result.Interface() } - return ToJSValue(c, jsValues) + return ToJsValue(c, jsValues) } // Single return value -> return that value if len(results) == 1 { - return ToJSValue(c, results[0].Interface()) + return ToJsValue(c, results[0].Interface()) } // Multiple return values -> return as JS array @@ -264,5 +264,5 @@ func GoFuncResultToJs(c *Context, results []reflect.Value) (*Value, error) { jsValues[i] = result.Interface() } - return ToJSValue(c, jsValues) + return ToJsValue(c, jsValues) } diff --git a/functojs_test.go b/functojs_test.go index 1b1179c..3a74210 100644 --- a/functojs_test.go +++ b/functojs_test.go @@ -58,7 +58,7 @@ func TestBasicConversion(t *testing.T) { t.Run("InvalidTypes", func(t *testing.T) { _, err := qjs.FuncToJS(ctx, "not a function") require.Error(t, err) - assert.Contains(t, err.Error(), "expected GO target function") + assert.Contains(t, err.Error(), "expected GO type function") }) t.Run("FunctionTypes", func(t *testing.T) { diff --git a/gotojs.go b/gotojs.go index 555d636..bc42a4c 100644 --- a/gotojs.go +++ b/gotojs.go @@ -8,13 +8,24 @@ import ( "time" ) -// ToJSValue converts any Go value to a QuickJS value. -func ToJSValue(c *Context, v any) (*Value, error) { - return NewTracker[uintptr]().ToJSValue(c, v) +// ToJsValue converts any Go value to a QuickJS value. +func ToJsValue(c *Context, v any) (*Value, error) { + jsVal, err := NewTracker[uintptr]().toJsValue(c, v) + if err != nil { + return nil, fmt.Errorf("[ToJSValue] %w", err) + } + + if !jsVal.IsNull() && jsVal.IsObject() { + goValueType := reflect.TypeOf(v).String() + jsVal.SetPropertyStr("__go_type", c.NewString(goValueType)) + jsVal.SetPropertyStr("__registry_id", c.NewInt64(int64(c.runtime.registry.Register(v)))) + } + + return jsVal, nil } -// ToJSValue converts any Go value to a QuickJS Value using the conversion context. -func (tracker *Tracker[T]) ToJSValue(c *Context, v any) (*Value, error) { +// toJsValue converts any Go value to a QuickJS Value using the conversion context. +func (tracker *Tracker[T]) toJsValue(c *Context, v any) (*Value, error) { if v == nil { return c.NewNull(), nil } @@ -34,9 +45,9 @@ func (tracker *Tracker[T]) ToJSValue(c *Context, v any) (*Value, error) { return tracker.convertReflectValue(c, v) } -// StructToJSObjectValue converts a Go struct to a JavaScript object. +// GoStructToJs converts a Go struct to a JavaScript object. // Includes both fields and methods as object properties. -func (tracker *Tracker[T]) StructToJSObjectValue( +func (tracker *Tracker[T]) GoStructToJs( c *Context, rtype reflect.Type, rval reflect.Value, @@ -70,19 +81,19 @@ func (tracker *Tracker[T]) StructToJSObjectValue( }) } -// SliceToArrayValue converts a Go slice to a JavaScript array. -func (tracker *Tracker[T]) SliceToArrayValue(c *Context, rval reflect.Value) (*Value, error) { +// GoSliceToJs converts a Go slice to a JavaScript array. +func (tracker *Tracker[T]) GoSliceToJs(c *Context, rval reflect.Value) (*Value, error) { return tracker.arrayLikeToJS(c, rval, "slice") } -// ArrayToArrayValue converts a Go array to a JavaScript array without unnecessary copying. -func (tracker *Tracker[T]) ArrayToArrayValue(c *Context, rval reflect.Value) (*Value, error) { +// GoArrayToJs converts a Go array to a JavaScript array without unnecessary copying. +func (tracker *Tracker[T]) GoArrayToJs(c *Context, rval reflect.Value) (*Value, error) { return tracker.arrayLikeToJS(c, rval, "array") } -// MapToObjectValue converts a Go map to a JavaScript object. +// GoMapToJs converts a Go map to a JavaScript object. // Non-string keys are converted to string representation. -func (tracker *Tracker[T]) MapToObjectValue( +func (tracker *Tracker[T]) GoMapToJs( c *Context, rval reflect.Value, ) (*Value, error) { @@ -97,7 +108,7 @@ func (tracker *Tracker[T]) MapToObjectValue( keyStr = fmt.Sprintf("%v", key.Interface()) } - jsValue, err := tracker.ToJSValue(c, value.Interface()) + jsValue, err := tracker.toJsValue(c, value.Interface()) if err != nil { return newGoToJsErr("map key: "+keyStr, err) } @@ -109,8 +120,8 @@ func (tracker *Tracker[T]) MapToObjectValue( }) } -// GoNumberToJS converts Go numeric types to appropriate JS number types. -func GoNumberToJS[T NumberType](c *Context, i T) *Value { +// GoNumberToJs converts Go numeric types to appropriate JS number types. +func GoNumberToJs[T NumberType](c *Context, i T) *Value { switch v := any(i).(type) { case int32: return c.NewInt32(v) @@ -139,8 +150,8 @@ func GoNumberToJS[T NumberType](c *Context, i T) *Value { } } -// GoComplexToJS converts Go complex numbers to JS objects with real/imag properties. -func GoComplexToJS[T complex64 | complex128](c *Context, z T) *Value { +// GoComplexToJs converts Go complex numbers to JS objects with real/imag properties. +func GoComplexToJs[T complex64 | complex128](c *Context, z T) *Value { obj := c.NewObject() var realPart, imagPart float64 @@ -160,23 +171,23 @@ func GoComplexToJS[T complex64 | complex128](c *Context, z T) *Value { return obj } -// StructToJSObjectValue provides backward compatibility for the old API. -func StructToJSObjectValue( +// GoStructToJs converts Go structs to JavaScript objects. +func GoStructToJs( c *Context, rtype reflect.Type, rval reflect.Value, ) (*Value, error) { - return NewTracker[uint64]().StructToJSObjectValue(c, rtype, rval) + return NewTracker[uint64]().GoStructToJs(c, rtype, rval) } -// SliceToArrayValue provides backward compatibility for the old API. -func SliceToArrayValue(c *Context, rval reflect.Value) (*Value, error) { - return NewTracker[uint64]().SliceToArrayValue(c, rval) +// GoSliceToJs converts Go slices to JavaScript arrays. +func GoSliceToJs(c *Context, rval reflect.Value) (*Value, error) { + return NewTracker[uint64]().GoSliceToJs(c, rval) } -// MapToObjectValue provides backward compatibility for the old API. -func MapToObjectValue(c *Context, rval reflect.Value) (*Value, error) { - return NewTracker[uint64]().MapToObjectValue(c, rval) +// GoMapToJs converts Go maps to JavaScript objects. +func GoMapToJs(c *Context, rval reflect.Value) (*Value, error) { + return NewTracker[uint64]().GoMapToJs(c, rval) } // tryConvertBuiltinTypes handles built-in Go types that don't require reflection. @@ -211,38 +222,38 @@ func tryConvertBuiltinTypes(c *Context, v any) *Value { func tryConvertNumeric(c *Context, v any) *Value { switch val := v.(type) { case int8: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case int16: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case int32: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case int: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case int64: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case uint8: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case uint16: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case uint32: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case uint: - return GoNumberToJS(c, val) + return GoNumberToJs(c, val) case uint64: // Large uint64 values use float64 to avoid overflow if val&(uint64(1)< math.MaxInt64 { return c.NewFloat64(float64(uintVal)), nil } - return GoNumberToJS(c, int64(uintVal)), nil + return GoNumberToJs(c, int64(uintVal)), nil case reflect.Float32, reflect.Float64: - return GoNumberToJS(c, rval.Float()), nil + return GoNumberToJs(c, rval.Float()), nil case reflect.String: return c.NewString(rval.String()), nil case reflect.Bool: @@ -422,7 +433,7 @@ func (tracker *Tracker[T]) addEmbeddedPrimitive( return nil } - prop, err := tracker.ToJSValue(c, fieldValue.Interface()) + prop, err := tracker.toJsValue(c, fieldValue.Interface()) obj.SetPropertyStr(fieldName, prop) return err @@ -447,7 +458,7 @@ func (tracker *Tracker[T]) processRegularFields( continue } - prop, err := tracker.ToJSValue(c, rval.Field(i).Interface()) + prop, err := tracker.toJsValue(c, rval.Field(i).Interface()) if err != nil { return err } @@ -512,7 +523,7 @@ func (tracker *Tracker[T]) arrayLikeToJS( for i := range rval.Len() { elem := rval.Index(i) - jsElem, err := tracker.ToJSValue(c, elem.Interface()) + jsElem, err := tracker.toJsValue(c, elem.Interface()) if err != nil { arr.Free() diff --git a/gotojs_test.go b/gotojs_test.go index 4b66c7f..c8cf36a 100644 --- a/gotojs_test.go +++ b/gotojs_test.go @@ -164,7 +164,7 @@ func createNestedStructure(depth int) any { } func testValueConversion(t *testing.T, ctx *qjs.Context, input any, validator func(*qjs.Value)) { - result, err := qjs.ToJSValue(ctx, input) + result, err := qjs.ToJsValue(ctx, input) require.NoError(t, err, "ToJSValue should not return error for input %T: %v", input, input) require.NotNil(t, result, "ToJSValue should not return nil for input %T: %v", input, input) defer result.Free() @@ -172,7 +172,7 @@ func testValueConversion(t *testing.T, ctx *qjs.Context, input any, validator fu } func testErrorCase(t *testing.T, ctx *qjs.Context, input any, expectedErrorSubstring string) { - result, err := qjs.ToJSValue(ctx, input) + result, err := qjs.ToJsValue(ctx, input) if result != nil { result.Free() } @@ -245,7 +245,7 @@ func TestJSValueConversion(t *testing.T) { } t.Run("ByteRuneComplexTypes", func(t *testing.T) { - jsValue, err := qjs.ToJSValue(runtime.Context(), []byte("hello world")) + jsValue, err := qjs.ToJsValue(runtime.Context(), []byte("hello world")) require.NoError(t, err) defer jsValue.Free() @@ -265,7 +265,7 @@ func TestJSValueConversion(t *testing.T) { }) t.Run("Uintptr", func(t *testing.T) { - val := qjs.GoNumberToJS[uintptr](ctx, 42) + val := qjs.GoNumberToJs[uintptr](ctx, 42) assert.True(t, val.IsNumber(), "Uintptr should be converted to number") retrieved := val.Int64() assert.Equal(t, int64(42), retrieved, "Uintptr value should match") @@ -284,7 +284,7 @@ func TestJSValueConversion(t *testing.T) { }) t.Run("MaxUint64", func(t *testing.T) { - val := qjs.GoNumberToJS[uint64](ctx, math.MaxUint64) + val := qjs.GoNumberToJs[uint64](ctx, math.MaxUint64) assert.True(t, val.IsBigInt()) retrieved := val.BigInt().Uint64() var expected uint64 = math.MaxUint64 @@ -958,7 +958,7 @@ func TestJSValueConversion(t *testing.T) { rtype := reflect.TypeOf(s) rval := reflect.ValueOf(s) - result, err := qjs.StructToJSObjectValue(ctx, rtype, rval) + result, err := qjs.GoStructToJs(ctx, rtype, rval) require.NoError(t, err) defer result.Free() @@ -969,7 +969,7 @@ func TestJSValueConversion(t *testing.T) { slice := []int{1, 2, 3} rval := reflect.ValueOf(slice) - result, err := qjs.SliceToArrayValue(ctx, rval) + result, err := qjs.GoSliceToJs(ctx, rval) require.NoError(t, err) defer result.Free() @@ -980,7 +980,7 @@ func TestJSValueConversion(t *testing.T) { m := map[string]int{"key": 42} rval := reflect.ValueOf(m) - result, err := qjs.MapToObjectValue(ctx, rval) + result, err := qjs.GoMapToJs(ctx, rval) require.NoError(t, err) defer result.Free() @@ -1123,7 +1123,7 @@ func TestToJSValue_ErrorHandling(t *testing.T) { const maxDepth = 10 root := createNestedStructure(maxDepth) - jsValue, err := qjs.ToJSValue(runtime.Context(), root) + jsValue, err := qjs.ToJsValue(runtime.Context(), root) require.NoError(t, err) defer jsValue.Free() assert.True(t, jsValue.IsObject()) diff --git a/jstogo.go b/jstogo.go index abff5c9..814a93d 100644 --- a/jstogo.go +++ b/jstogo.go @@ -9,6 +9,105 @@ import ( "time" ) +func ToGoValue[T any](input *Value, samples ...T) (v T, err error) { + registryID := input.GetPropertyStr("__registry_id") + if !registryID.IsUndefined() && !registryID.IsNull() { + registryVal, ok := input.context.runtime.registry.Get(uint64(registryID.Int64())) + if ok { + val, ok := registryVal.(T) + if !ok { + return v, fmt.Errorf("type assertion failed for registry value: expected %T, got %T", v, registryVal) + } + + return val, nil + } + } + + return toGoValue(NewTracker[uint64](), input, samples...) +} + +func toGoValue[T any]( + tracker *Tracker[uint64], + input *Value, + samples ...T, +) (v T, err error) { + temp, sample := createTemp(samples...) + switch any(sample).(type) { + case *Value: + tvv, _ := any(input).(T) + + return tvv, nil + case Value: + tv, _ := any(*input).(T) + + return tv, nil + } + + defer func() { + v, err = processTempValue("ToGoValue", temp, err, sample) + }() + + // If JS value is a QJSProxyValue, extract the Go value from the registry + if input.IsQJSProxyValue() { + proxyID := input.GetPropertyStr("proxyId") + temp, _ = input.context.runtime.registry.Get(uint64(proxyID.Int64())) + + return v, nil + } + + var ok bool + if temp, ok, err = jsPrimitivesToGo(tracker, input, sample); ok { + return v, err + } + + if input.IsGlobalInstanceOf("Date") { + temp, err = JsTimeToGo(input) + + return v, err + } + + if input.IsGlobalInstanceOf("RegExp") { + temp = input.String() + + return v, err + } + + if input.IsByteArray() { + temp, err = JsArrayBufferToGo(input) + + return v, err + } + + if IsTypedArray(input) { + temp, err = JsTypedArrayToGo(input) + + return v, err + } + + if input.IsMap() { + temp, err = jsObjectToGo(tracker, input, sample) + + return v, err + } + + if input.IsSet() { + temp, err = jsSetToGoWithContext(tracker, input, sample) + + return v, err + } + + if input.IsArray() { + temp, err = jsArrayToGoWithContext(tracker, input, sample) + + return v, err + } + + // Fallback for all other object types + temp, err = jsObjectToGo(tracker, input, sample) + + return v, err +} + // JsNumberToGo converts JavaScript numbers to Go numeric types. func JsNumberToGo[T any](input *Value, samples ...T) (v T, err error) { temp, sample := createTemp(samples...) @@ -48,7 +147,7 @@ func JsNumberToGo[T any](input *Value, samples ...T) (v T, err error) { return v, err } - return v, newInvalidGoTargetErr("numeric type", sample) + return v, newInvalidGoTypeErr("numeric type", sample) } // JsArrayToGo handles conversion of JavaScript Array objects to Go types. @@ -75,7 +174,7 @@ func JsBigIntToGo[T any](input *Value, samples ...T) (v T, err error) { case big.Int: temp = *bigInt default: - return v, newInvalidGoTargetErr("*big.Int/big.Int", sample) + return v, newInvalidGoTypeErr("*big.Int/big.Int", sample) } return v, err @@ -173,14 +272,14 @@ func mapEntryToGoWithContext( ) (k, v reflect.Value, err error) { keySample := reflect.New(goKeyType).Elem().Interface() - goKeyFromJs, keyErr := jsValueToGo(ctx, jsKey, keySample) + goKeyFromJs, keyErr := toGoValue(ctx, jsKey, keySample) if keyErr != nil { return k, v, newJsToGoErr(jsKey, keyErr, "map key") } k = reflect.ValueOf(goKeyFromJs).Convert(goKeyType) valueSample := reflect.New(goValueType).Elem().Interface() - goValFromJs, valErr := jsValueToGo(ctx, jsValue, valueSample) + goValFromJs, valErr := toGoValue(ctx, jsValue, valueSample) switch { case valErr != nil: @@ -418,7 +517,7 @@ func setFieldWithDirectConversion( ) error { fieldSample := reflect.New(fieldType).Elem().Interface() - converted, err := jsValueToGo(tracker, jsValue, fieldSample) + converted, err := toGoValue(tracker, jsValue, fieldSample) if err != nil { return newJsToGoErr( jsValue, err, @@ -580,7 +679,7 @@ func convertArgsToJS( jsArgs := make([]*Value, 0, expectedArgsCount) convertArg := func(argValue any, argIndex int) error { - jsArg, convErr := ToJSValue(ctx, argValue) + jsArg, convErr := ToJsValue(ctx, argValue) if convErr != nil { results[len(results)-1] = reflect.ValueOf( fmt.Errorf( @@ -645,7 +744,7 @@ func handleJsFunctionResult( return results } - goResult, err := jsValueToGo[any](tracker, jsResult) + goResult, err := toGoValue[any](tracker, jsResult) if err != nil { results[len(results)-1] = reflect.ValueOf( fmt.Errorf("failed to convert JS function result '%s' to Go: %w", jsResult.Type(), err), @@ -674,92 +773,6 @@ func handleJsFunctionResult( return results } -func JsValueToGo[T any](input *Value, samples ...T) (v T, err error) { - return jsValueToGo(NewTracker[uint64](), input, samples...) -} - -func jsValueToGo[T any]( - tracker *Tracker[uint64], - input *Value, - samples ...T, -) (v T, err error) { - temp, sample := createTemp(samples...) - switch any(sample).(type) { - case *Value: - tvv, _ := any(input).(T) - - return tvv, nil - case Value: - tv, _ := any(*input).(T) - - return tv, nil - } - - defer func() { - v, err = processTempValue("ToGoValue", temp, err, sample) - }() - - // If JS value is a QJSProxyValue, extract the Go value from the registry - if input.IsQJSProxyValue() { - proxyID := input.GetPropertyStr("proxyId") - temp, _ = input.context.runtime.registry.Get(uint64(proxyID.Int64())) - - return v, nil - } - - var ok bool - if temp, ok, err = jsPrimitivesToGo(tracker, input, sample); ok { - return v, err - } - - if input.IsGlobalInstanceOf("Date") { - temp, err = JsTimeToGo(input) - - return v, err - } - - if input.IsGlobalInstanceOf("RegExp") { - temp = input.String() - - return v, err - } - - if input.IsByteArray() { - temp, err = JsArrayBufferToGo(input) - - return v, err - } - - if IsTypedArray(input) { - temp, err = JsTypedArrayToGo(input) - - return v, err - } - - if input.IsMap() { - temp, err = jsObjectToGo(tracker, input, sample) - - return v, err - } - - if input.IsSet() { - temp, err = jsSetToGoWithContext(tracker, input, sample) - - return v, err - } - - if input.IsArray() { - temp, err = jsArrayToGoWithContext(tracker, input, sample) - - return v, err - } - - // Fallback for all other object types - temp, err = jsObjectToGo(tracker, input, sample) - - return v, err -} - // jsPrimitivesToGo converts JavaScript primitive types to Go types. func jsPrimitivesToGo[T any]( tracker *Tracker[uint64], diff --git a/jstogo_test.go b/jstogo_test.go index 928d410..11dfa05 100644 --- a/jstogo_test.go +++ b/jstogo_test.go @@ -65,7 +65,7 @@ func TestJsBigIntToGo(t *testing.T) { t.Run("unsupported_type", func(t *testing.T) { val, err := qjs.JsBigIntToGo[int64](jsBigInt) assert.Error(t, err) - assert.Contains(t, err.Error(), "expected GO target *big.Int/big.Int") + assert.Contains(t, err.Error(), "expected GO type *big.Int/big.Int") assert.Equal(t, int64(0), val) }) } @@ -1877,7 +1877,7 @@ func TestJsFuncToGo(t *testing.T) { jsCode: `(a, b) => a + b`, sample: "not a function", expectErr: true, - errMsg: "expected GO target function", + errMsg: "expected GO type function", }, { name: "function_with_single_return_value", @@ -2236,6 +2236,41 @@ func TestJsFuncToGo(t *testing.T) { assert.Empty(t, result) }) }) + + // this test pass go function to js without calling it in js side + // then pass it back to go and call it in go side + // to verify the go function passthrough works correctly + // without any modification by js engine + t.Run("Passthrough_of_go_func", func(t *testing.T) { + rt := must(qjs.New()) + defer rt.Close() + + type TestArg struct { + TestFunc func(int) (int, error) `json:"testFunc"` + } + + goArg := must(qjs.ToJsValue(rt.Context(), TestArg{ + TestFunc: func(x int) (int, error) { + return x * x, nil + }, + })) + rt.Context().Global().SetPropertyStr("goArg", goArg) + goReceiver := rt.Context().Function(func(ctx *qjs.This) (*qjs.Value, error) { + jsArg := ctx.Args()[0] + // expect wrong type + _, err := qjs.ToGoValue(jsArg, "") + assert.Error(t, err, "string is not a valid type, expected TestArg") + + goArg := must(qjs.ToGoValue(jsArg, TestArg{})) + result := must(goArg.TestFunc(5)) + assert.Equal(t, 25, result) + return nil, nil + }) + rt.Context().Global().SetPropertyStr("goReceiver", goReceiver) + jsCode := `(function() { goReceiver(goArg); })();` + result := must(rt.Eval("test.js", qjs.Code(jsCode))) + defer result.Free() + }) } func TestJsValueToGo(t *testing.T) { @@ -2508,9 +2543,9 @@ func TestJsValueToGo(t *testing.T) { var result any if test.sample != nil { - result, err = qjs.JsValueToGo(jsValue, test.sample) + result, err = qjs.ToGoValue(jsValue, test.sample) } else { - result, err = qjs.JsValueToGo[any](jsValue) + result, err = qjs.ToGoValue[any](jsValue) } if test.customAssert != nil { @@ -2587,12 +2622,12 @@ func TestStringToBoolConversion(t *testing.T) { result := must(rt.Eval("test.js", qjs.Code(`"non-empty"`))) defer result.Free() - converted := must(qjs.JsValueToGo(result, boolVal)) + converted := must(qjs.ToGoValue(result, boolVal)) assert.True(t, converted) emptyResult := must(rt.Eval("test.js", qjs.Code(`""`))) defer emptyResult.Free() - converted2 := must(qjs.JsValueToGo(emptyResult, boolVal)) + converted2 := must(qjs.ToGoValue(emptyResult, boolVal)) assert.False(t, converted2) } @@ -2607,7 +2642,7 @@ func TestErrorValueHandling(t *testing.T) { defer errorValue.Free() assert.True(t, errorValue.IsError()) - converted := must(qjs.JsValueToGo[error](errorValue)) + converted := must(qjs.ToGoValue[error](errorValue)) assert.Error(t, converted) assert.Contains(t, converted.Error(), "not a constructor") } @@ -2620,13 +2655,13 @@ func TestStringToNumericConversion(t *testing.T) { result := must(rt.Eval("test.js", qjs.Code(`"42"`))) defer result.Free() var ptrInt *int - converted := must(qjs.JsValueToGo(result, ptrInt)) + converted := must(qjs.ToGoValue(result, ptrInt)) assert.Equal(t, 42, *converted) floatResult := must(rt.Eval("test.js", qjs.Code(`"3.14"`))) defer floatResult.Free() var ptrFloat *float64 - converted2 := must(qjs.JsValueToGo(floatResult, ptrFloat)) + converted2 := must(qjs.ToGoValue(floatResult, ptrFloat)) assert.Equal(t, 3.14, *converted2) }) @@ -2635,7 +2670,7 @@ func TestStringToNumericConversion(t *testing.T) { defer result.Free() var intVal int - _, err := qjs.JsValueToGo(result, intVal) + _, err := qjs.ToGoValue(result, intVal) assert.Error(t, err) assert.Contains(t, err.Error(), "empty string cannot be converted to number") }) @@ -2681,7 +2716,7 @@ func TestStringToNumericConversion(t *testing.T) { result := must(rt.Eval("test.js", qjs.Code(tc.jsValue))) defer result.Free() - converted, err := qjs.JsValueToGo(result, tc.targetType) + converted, err := qjs.ToGoValue(result, tc.targetType) require.NoError(t, err) assert.Equal(t, tc.expected, converted) }) diff --git a/runtime_test.go b/runtime_test.go index 636d624..0c0a6bb 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -39,7 +39,7 @@ func testConcurrentRuntimeExecution(t *testing.T, threadID int) { Name: threadName, } - jsValue, err := qjs.ToJSValue(rt.Context(), data) + jsValue, err := qjs.ToJsValue(rt.Context(), data) assert.NoError(t, err) if jsValue != nil { assert.True(t, jsValue.IsObject()) @@ -84,7 +84,7 @@ func testPooledRuntimeExecution(t *testing.T, pool *qjs.Pool, workerID int) { "processed": true, } - jsValue, err := qjs.ToJSValue(rt.Context(), testData) + jsValue, err := qjs.ToJsValue(rt.Context(), testData) require.NoError(t, err) defer jsValue.Free() diff --git a/value.go b/value.go index 79569d0..ace6eec 100644 --- a/value.go +++ b/value.go @@ -325,7 +325,7 @@ func (v *Value) DeleteProperty(name string) bool { func (v *Value) Invoke(fname string, args ...any) (_ *Value, err error) { jsArgs := make([]*Value, len(args)) for i, arg := range args { - if jsArgs[i], err = ToJSValue(v.context, arg); err != nil { + if jsArgs[i], err = ToJsValue(v.context, arg); err != nil { return nil, err } defer jsArgs[i].Free()