This commit is contained in:
2026-01-27 16:20:46 -05:00
parent 7bf4f115b1
commit c3a16c9e92
21 changed files with 1192 additions and 577 deletions

View File

@@ -0,0 +1,77 @@
package builtin
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dop251/goja"
)
type TestArgs struct {
Field1 string `json:"field1"`
}
func (t TestArgs) Validate() error {
return nil
}
func TestAsyncBuiltin(t *testing.T) {
RegisterAsyncBuiltin[TestArgs, string]("testAsync", func(_ context.Context, args TestArgs) (string, error) {
return "result: " + args.Field1, nil
})
registryMutex.RLock()
builtin, ok := builtinRegistry["testAsync"]
registryMutex.RUnlock()
require.True(t, ok, "testAsync should be registered")
assert.Contains(t, builtin.Definition, "Promise<string>", "definition should include Promise<string>")
}
func TestAsyncBuiltinResolution(t *testing.T) {
RegisterAsyncBuiltin[TestArgs, string]("resolveTest", func(_ context.Context, args TestArgs) (string, error) {
return "test-result", nil
})
vm := goja.New()
RegisterBuiltins(vm)
result, err := vm.RunString(`resolveTest({field1: "hello"})`)
require.NoError(t, err)
promise, ok := result.Export().(*goja.Promise)
require.True(t, ok, "should return a Promise")
assert.NotNil(t, promise)
}
func TestAsyncBuiltinRejection(t *testing.T) {
RegisterAsyncBuiltin[TestArgs, string]("rejectTest", func(_ context.Context, args TestArgs) (string, error) {
return "", assert.AnError
})
vm := goja.New()
RegisterBuiltins(vm)
result, err := vm.RunString(`rejectTest({field1: "hello"})`)
require.NoError(t, err)
promise, ok := result.Export().(*goja.Promise)
require.True(t, ok, "should return a Promise")
assert.NotNil(t, promise)
}
func TestNonPromise(t *testing.T) {
RegisterBuiltin[TestArgs, string]("nonPromiseTest", func(_ context.Context, args TestArgs) (string, error) {
return "sync-result", nil
})
vm := goja.New()
RegisterBuiltins(vm)
result, err := vm.RunString(`nonPromiseTest({field1: "hello"})`)
require.NoError(t, err)
assert.Equal(t, "sync-result", result.Export())
}

View File

@@ -0,0 +1,170 @@
package builtin
import (
"fmt"
"reflect"
"strings"
"sync"
)
type typeCollector struct {
mu sync.Mutex
types map[string]string
paramTypes map[string]bool
}
func newTypeCollector() *typeCollector {
return &typeCollector{
types: make(map[string]string),
paramTypes: make(map[string]bool),
}
}
func (tc *typeCollector) collectTypes(argsType reflect.Type, fnType reflect.Type) []string {
tc.mu.Lock()
defer tc.mu.Unlock()
tc.types = make(map[string]string)
tc.paramTypes = make(map[string]bool)
var result []string
tc.collectStruct(argsType, argsType.Name())
for i := 0; i < argsType.NumField(); i++ {
field := argsType.Field(i)
if field.Type.Kind() == reflect.Pointer || strings.Contains(field.Tag.Get("json"), ",omitempty") {
tc.collectParamType(field.Type)
}
}
if fnType.Kind() == reflect.Func && fnType.NumOut() > 0 {
lastIndex := fnType.NumOut() - 1
lastType := fnType.Out(lastIndex)
if lastType.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if fnType.NumOut() > 1 {
tc.collectType(fnType.Out(0))
}
} else {
tc.collectType(lastType)
}
}
for _, t := range tc.types {
result = append(result, t)
}
return result
}
func (tc *typeCollector) collectParamType(t reflect.Type) {
if t.Kind() == reflect.Pointer {
tc.collectParamType(t.Elem())
return
}
if t.Kind() == reflect.Struct && t.Name() != "" {
tc.paramTypes[t.Name()+" | null"] = true
}
}
func (tc *typeCollector) getParamTypes() map[string]bool {
return tc.paramTypes
}
func (tc *typeCollector) collectType(t reflect.Type) {
if t.Kind() == reflect.Pointer {
tc.collectType(t.Elem())
return
}
if t.Kind() == reflect.Struct {
name := t.Name()
if _, exists := tc.types[name]; !exists {
tc.collectStruct(t, name)
}
}
}
func (tc *typeCollector) collectStruct(t reflect.Type, name string) {
if t.Kind() != reflect.Struct {
return
}
var fields []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Anonymous || !field.IsExported() {
continue
}
fieldName := getFieldName(field)
var tsType string
var isOptional bool
isPointer := field.Type.Kind() == reflect.Pointer
if isPointer {
isOptional = true
tsType = goTypeToTSType(field.Type, false)
} else {
isOptional = strings.Contains(field.Tag.Get("json"), ",omitempty")
tsType = goTypeToTSType(field.Type, false)
}
if isOptional {
fieldName += "?"
}
fields = append(fields, fmt.Sprintf("%s: %s", fieldName, tsType))
tc.collectType(field.Type)
}
tc.types[name] = fmt.Sprintf("interface %s {%s}", name, strings.Join(fields, "; "))
}
func goTypeToTSType(t reflect.Type, isPointer bool) string {
if t.Kind() == reflect.Pointer {
return goTypeToTSType(t.Elem(), true)
}
baseType := ""
switch t.Kind() {
case reflect.String:
baseType = "string"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
baseType = "number"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
baseType = "number"
case reflect.Float32, reflect.Float64:
baseType = "number"
case reflect.Bool:
baseType = "boolean"
case reflect.Interface:
baseType = "any"
case reflect.Slice:
baseType = fmt.Sprintf("%s[]", goTypeToTSType(t.Elem(), false))
case reflect.Map:
if t.Key().Kind() == reflect.String && t.Elem().Kind() == reflect.Interface {
baseType = "Record<string, any>"
} else {
baseType = "Record<string, any>"
}
case reflect.Struct:
name := t.Name()
if name == "" {
baseType = "{}"
} else {
baseType = name
}
default:
baseType = "any"
}
if isPointer {
baseType += " | null"
}
return baseType
}

204
internal/builtin/convert.go Normal file
View File

@@ -0,0 +1,204 @@
package builtin
import (
"fmt"
"reflect"
"strings"
"github.com/dop251/goja"
)
func convertJSValueToGo(vm *goja.Runtime, jsValue goja.Value, targetType reflect.Type) (any, error) {
if goja.IsNull(jsValue) {
if targetType.Kind() == reflect.Pointer || targetType.Kind() == reflect.Map {
return nil, nil
}
return nil, fmt.Errorf("cannot convert null/undefined to %v", targetType)
}
if goja.IsUndefined(jsValue) {
if targetType.Kind() == reflect.Pointer || targetType.Kind() == reflect.Map {
return nil, nil
}
return nil, fmt.Errorf("cannot convert null/undefined to %v", targetType)
}
switch targetType.Kind() {
case reflect.String:
return jsValue.String(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, ok := jsValue.Export().(int64)
if !ok {
return nil, fmt.Errorf("expected int, got %T", jsValue.Export())
}
return reflect.ValueOf(n).Convert(targetType).Interface(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, ok := jsValue.Export().(int64)
if !ok {
return nil, fmt.Errorf("expected uint, got %T", jsValue.Export())
}
return reflect.ValueOf(uint(n)).Convert(targetType).Interface(), nil
case reflect.Float32, reflect.Float64:
n, ok := jsValue.Export().(float64)
if !ok {
return nil, fmt.Errorf("expected float, got %T", jsValue.Export())
}
return reflect.ValueOf(n).Convert(targetType).Interface(), nil
case reflect.Bool:
return jsValue.ToBoolean(), nil
case reflect.Interface:
return jsValue.Export(), nil
case reflect.Map:
if goja.IsUndefined(jsValue) || goja.IsNull(jsValue) {
return nil, nil
}
if targetType.Key().Kind() == reflect.String {
obj := jsValue.ToObject(vm)
if obj == nil {
return nil, fmt.Errorf("not an object")
}
if targetType.Elem().Kind() == reflect.Interface {
result := make(map[string]any)
for _, key := range obj.Keys() {
result[key] = obj.Get(key).Export()
}
return result, nil
} else if targetType.Elem().Kind() == reflect.String {
result := make(map[string]string)
for _, key := range obj.Keys() {
v := obj.Get(key)
result[key] = v.String()
}
return result, nil
}
}
return nil, fmt.Errorf("unsupported map type: %v", targetType)
case reflect.Struct:
obj := jsValue.ToObject(vm)
if obj == nil {
return nil, fmt.Errorf("not an object")
}
result := reflect.New(targetType).Elem()
for i := 0; i < targetType.NumField(); i++ {
field := targetType.Field(i)
fieldName := getFieldName(field)
jsField := obj.Get(fieldName)
var err error
var converted any
func() {
defer func() {
if r := recover(); r != nil {
err = nil
converted = nil
}
}()
converted, err = convertJSValueToGo(vm, jsField, field.Type)
}()
if err != nil {
return nil, fmt.Errorf("field %s: %v", fieldName, err)
}
if converted == nil {
if field.Type.Kind() == reflect.Pointer || field.Type.Kind() == reflect.Map {
continue
}
} else {
result.Field(i).Set(reflect.ValueOf(converted))
}
}
return result.Interface(), nil
case reflect.Pointer:
if goja.IsNull(jsValue) || goja.IsUndefined(jsValue) {
return nil, nil
}
elemType := targetType.Elem()
converted, err := convertJSValueToGo(vm, jsValue, elemType)
if err != nil {
return nil, err
}
ptr := reflect.New(elemType)
ptr.Elem().Set(reflect.ValueOf(converted))
return ptr.Interface(), nil
default:
return nil, fmt.Errorf("unsupported type: %v", targetType)
}
}
func convertGoValueToJS(vm *goja.Runtime, goValue reflect.Value) goja.Value {
value := goValue.Interface()
switch v := value.(type) {
case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool:
return vm.ToValue(v)
case error:
return vm.ToValue(v.Error())
case map[string]string:
obj := vm.NewObject()
for key, val := range v {
_ = obj.Set(key, val)
}
return obj
case map[string]any:
obj := vm.NewObject()
for key, val := range v {
_ = obj.Set(key, convertGoValueToJS(vm, reflect.ValueOf(val)))
}
return obj
case []any:
arr := make([]goja.Value, len(v))
for i, item := range v {
arr[i] = convertGoValueToJS(vm, reflect.ValueOf(item))
}
return vm.ToValue(arr)
default:
if goValue.Kind() == reflect.Pointer {
if goValue.IsNil() {
return goja.Null()
}
return convertGoValueToJS(vm, goValue.Elem())
}
if goValue.Kind() == reflect.Struct {
obj := vm.NewObject()
for i := 0; i < goValue.NumField(); i++ {
field := goValue.Type().Field(i)
fieldName := getFieldName(field)
_ = obj.Set(fieldName, convertGoValueToJS(vm, goValue.Field(i)))
}
return obj
}
return vm.ToValue(v)
}
}
func getFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
name, _, _ := strings.Cut(jsonTag, ",")
return name
}
return field.Name
}

View File

@@ -0,0 +1,90 @@
package builtin
import (
"fmt"
"reflect"
"strings"
"sync"
"github.com/dop251/goja"
)
var (
builtinRegistry = make(map[string]Builtin)
registryMutex sync.RWMutex
collector *typeCollector
)
func registerBuiltin[T Args, R any](name string, isAsync bool, fn Func[T, R]) {
if collector == nil {
collector = newTypeCollector()
}
var zeroT T
tType := reflect.TypeOf(zeroT)
if tType.Kind() != reflect.Struct {
panic(fmt.Sprintf("builtin %s: argument must be a struct type, got %v", name, tType))
}
fnType := reflect.TypeOf(fn)
wrapper := createWrapper(fn, isAsync)
types := collector.collectTypes(tType, fnType)
paramTypes := collector.getParamTypes()
registryMutex.Lock()
b := Builtin{
Name: name,
Function: wrapper,
Definition: generateTypeScriptDefinition(name, tType, fnType, isAsync, paramTypes),
Types: types,
ParamTypes: paramTypes,
}
builtinRegistry[name] = b
registryMutex.Unlock()
}
func GetBuiltinsDeclarations() string {
registryMutex.RLock()
defer registryMutex.RUnlock()
typeDefinitions := make(map[string]bool)
var typeDefs []string
var functionDecls []string
for _, builtin := range builtinRegistry {
for _, t := range builtin.Types {
if !typeDefinitions[t] {
typeDefinitions[t] = true
typeDefs = append(typeDefs, t)
}
}
functionDecls = append(functionDecls, builtin.Definition)
}
result := strings.Join(typeDefs, "\n\n")
if len(result) > 0 && len(functionDecls) > 0 {
result += "\n\n"
}
result += strings.Join(functionDecls, "\n")
return result
}
func RegisterBuiltin[T Args, R any](name string, fn Func[T, R]) {
registerBuiltin(name, false, fn)
}
func RegisterAsyncBuiltin[T Args, R any](name string, fn Func[T, R]) {
registerBuiltin(name, true, fn)
}
func RegisterBuiltins(vm *goja.Runtime) {
registryMutex.RLock()
defer registryMutex.RUnlock()
for name, builtin := range builtinRegistry {
_ = vm.Set(name, builtin.Function(vm))
}
}

27
internal/builtin/types.go Normal file
View File

@@ -0,0 +1,27 @@
package builtin
import (
"context"
"github.com/dop251/goja"
)
type Builtin struct {
Name string
Function func(*goja.Runtime) func(goja.FunctionCall) goja.Value
Definition string
Types []string
ParamTypes map[string]bool
}
func (b *Builtin) HasParamType(typeName string) bool {
return b.ParamTypes[typeName]
}
type EmptyArgs struct{}
type Args interface {
Validate() error
}
type Func[T Args, R any] func(ctx context.Context, args T) (R, error)

View File

@@ -0,0 +1,64 @@
package builtin
import (
"fmt"
"reflect"
"strings"
)
func generateTypeScriptDefinition(name string, argsType reflect.Type, fnType reflect.Type, isPromise bool, paramTypes map[string]bool) string {
if argsType.Kind() != reflect.Struct {
return ""
}
var params []string
for i := 0; i < argsType.NumField(); i++ {
field := argsType.Field(i)
fieldName := getFieldName(field)
goType := field.Type
var tsType string
var isOptional bool
isPointer := goType.Kind() == reflect.Pointer
if isPointer {
isOptional = true
tsType = goTypeToTSType(goType, true)
if !strings.Contains(tsType, " | null") {
tsType += " | null"
}
} else {
isOptional = strings.Contains(field.Tag.Get("json"), ",omitempty")
tsType = goTypeToTSType(goType, false)
if isOptional && paramTypes[tsType+" | null"] {
tsType += " | null"
}
}
if isOptional {
fieldName += "?"
}
params = append(params, fmt.Sprintf("%s: %s", fieldName, tsType))
}
returnSignature := "any"
if fnType.Kind() == reflect.Func && fnType.NumOut() > 0 {
lastIndex := fnType.NumOut() - 1
lastType := fnType.Out(lastIndex)
if lastType.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if fnType.NumOut() > 1 {
returnType := fnType.Out(0)
returnSignature = goTypeToTSType(returnType, returnType.Kind() == reflect.Pointer)
}
} else {
returnSignature = goTypeToTSType(lastType, lastType.Kind() == reflect.Pointer)
}
}
if isPromise {
returnSignature = fmt.Sprintf("Promise<%s>", returnSignature)
}
return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature)
}

View File

@@ -0,0 +1,273 @@
package builtin
import (
"context"
"reflect"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
type TestBasicArgs struct {
Name string `json:"name"`
Age int `json:"age"`
}
func (t TestBasicArgs) Validate() error { return nil }
func TestBasicType(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestBasicArgs, string]("basic", func(ctx context.Context, args TestBasicArgs) (string, error) {
return args.Name, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function basic(name: string, age: number): string;")
assert.Contains(t, defs, "interface TestBasicArgs")
}
func resetRegistry() {
registryLock.Lock()
defer registryLock.Unlock()
builtinRegistry = make(map[string]Builtin)
}
var (
registryLock sync.Mutex
)
type TestComplexArgs struct {
Items []int `json:"items"`
Data map[string]any `json:"data"`
Flag bool `json:"flag"`
}
func (t TestComplexArgs) Validate() error { return nil }
func TestComplexTypes(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestComplexArgs, bool]("complex", func(ctx context.Context, args TestComplexArgs) (bool, error) {
return args.Flag, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function complex(items: number[], data: Record<string, any>, flag: boolean): boolean;")
}
type TestNestedArgs struct {
User struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
} `json:"user"`
}
func (t TestNestedArgs) Validate() error { return nil }
func TestNestedStruct(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestNestedArgs, string]("nested", func(ctx context.Context, args TestNestedArgs) (string, error) {
return args.User.FirstName, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function nested(user: {}): string;")
}
type TestOptionalArgs struct {
Name string `json:"name"`
Age *int `json:"age,omitempty"`
Score *int `json:"score"`
}
func (t TestOptionalArgs) Validate() error { return nil }
func TestOptionalFields(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestOptionalArgs, string]("optional", func(ctx context.Context, args TestOptionalArgs) (string, error) {
return args.Name, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function optional(name: string, age?: number | null, score?: number | null): string;")
}
type TestResult struct {
ID int `json:"id"`
Data []byte `json:"data"`
}
type TestResultArgs struct {
Input string `json:"input"`
}
func (t TestResultArgs) Validate() error { return nil }
func TestResultStruct(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestResultArgs, TestResult]("result", func(ctx context.Context, args TestResultArgs) (TestResult, error) {
return TestResult{ID: 1}, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function result(input: string): TestResult;")
assert.Contains(t, defs, "interface TestResult {id: number; data: number[]}")
}
type TestAsyncArgs struct {
URL string `json:"url"`
}
func (t TestAsyncArgs) Validate() error { return nil }
type TestAsyncResult struct {
Status int `json:"status"`
}
func TestAsyncPromise(t *testing.T) {
resetRegistry()
RegisterAsyncBuiltin[TestAsyncArgs, *TestAsyncStatus]("async", func(ctx context.Context, args TestAsyncArgs) (*TestAsyncStatus, error) {
return &TestAsyncStatus{Code: 200}, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function async(url: string): Promise<TestAsyncStatus | null>;")
assert.Contains(t, defs, "interface TestAsyncStatus")
}
type TestAsyncStatus struct {
Code int `json:"code"`
}
type TestNestedPointerResult struct {
Value string `json:"value"`
}
type TestNestedPointerArgs struct {
ID int `json:"id"`
}
func (t TestNestedPointerArgs) Validate() error { return nil }
func TestNestedPointerInResult(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestNestedPointerArgs, *TestNestedPointerResult]("pointerResult", func(ctx context.Context, args TestNestedPointerArgs) (*TestNestedPointerResult, error) {
return &TestNestedPointerResult{Value: "test"}, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function pointerResult(id: number): TestNestedPointerResult | null;")
}
type TestUintArgs struct {
Value uint `json:"value"`
}
func (t TestUintArgs) Validate() error { return nil }
func TestUintType(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestUintArgs, uint]("uint", func(ctx context.Context, args TestUintArgs) (uint, error) {
return args.Value, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function uint(value: number): number;")
}
type TestFloatArgs struct {
Amount float64 `json:"amount"`
}
func (t TestFloatArgs) Validate() error { return nil }
func TestFloatType(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestFloatArgs, float32]("float", func(ctx context.Context, args TestFloatArgs) (float32, error) {
return float32(args.Amount), nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function float(amount: number): number;")
}
type TestPointerInArgs struct {
User *struct {
Name string `json:"name"`
} `json:"user"`
}
func (t TestPointerInArgs) Validate() error { return nil }
func TestNestedPointerStruct(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestPointerInArgs, string]("nestedPointer", func(ctx context.Context, args TestPointerInArgs) (string, error) {
return "test", nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function nestedPointer(user?: {} | null): string;")
}
type TestErrorOnlyArgs struct {
Input string `json:"input"`
}
func (t TestErrorOnlyArgs) Validate() error { return nil }
func TestErrorOnlyReturn(t *testing.T) {
resetRegistry()
RegisterBuiltin[TestErrorOnlyArgs, struct{}]("errorOnly", func(ctx context.Context, args TestErrorOnlyArgs) (struct{}, error) {
return struct{}{}, nil
})
defs := GetBuiltinsDeclarations()
assert.Contains(t, defs, "declare function errorOnly(input: string): {};")
}
func TestGoTypeToTSTypeBasic(t *testing.T) {
tests := []struct {
input reflect.Type
inputPtr bool
expected string
}{
{reflect.TypeOf(""), false, "string"},
{reflect.TypeOf(0), false, "number"},
{reflect.TypeOf(int64(0)), false, "number"},
{reflect.TypeOf(uint(0)), false, "number"},
{reflect.TypeOf(3.14), false, "number"},
{reflect.TypeOf(float32(0.0)), false, "number"},
{reflect.TypeOf(true), false, "boolean"},
{reflect.TypeOf([]string{}), false, "string[]"},
{reflect.TypeOf([]int{}), false, "number[]"},
{reflect.TypeOf(map[string]any{}), false, "Record<string, any>"},
{reflect.TypeOf(map[string]int{}), false, "Record<string, any>"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := goTypeToTSType(tt.input, tt.inputPtr)
assert.Equal(t, tt.expected, result)
})
}
}
type TestNestedStructField struct {
Inner struct {
Name string `json:"name"`
} `json:"inner"`
}
func TestGoTypeToTSTypeNestedStruct(t *testing.T) {
result := goTypeToTSType(reflect.TypeOf(TestNestedStructField{}), false)
assert.Equal(t, "TestNestedStructField", result)
}
type TestArrayField struct {
Items []string `json:"items"`
}
func TestGoTypeToTSTypeArray(t *testing.T) {
result := goTypeToTSType(reflect.TypeOf(TestArrayField{}), false)
assert.Equal(t, "TestArrayField", result)
}

View File

@@ -0,0 +1,72 @@
package builtin
import (
"context"
"fmt"
"reflect"
"github.com/dop251/goja"
)
func createWrapper[T Args, R any](fn Func[T, R], isAsync bool) func(*goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(vm *goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
var args T
argsValue := reflect.ValueOf(&args).Elem()
for i := 0; i < argsValue.NumField() && i < len(call.Arguments); i++ {
jsArg := call.Arguments[i]
field := argsValue.Field(i)
if goja.IsUndefined(jsArg) || goja.IsNull(jsArg) {
if field.Kind() == reflect.Pointer {
continue
}
}
converted, err := convertJSValueToGo(vm, jsArg, field.Type())
if err != nil {
panic(fmt.Sprintf("argument %d (%s): %v", i, getFieldName(argsValue.Type().Field(i)), err))
}
if converted != nil {
field.Set(reflect.ValueOf(converted))
}
}
if err := args.Validate(); err != nil {
panic(fmt.Sprintf("argument validation failed: %v", err))
}
if isAsync {
return createAsyncPromise(vm, fn, args)
}
ctx := context.Background()
result, err := fn(ctx, args)
if err != nil {
panic(err)
}
return convertGoValueToJS(vm, reflect.ValueOf(result))
}
}
}
func createAsyncPromise[T Args, R any](vm *goja.Runtime, fn Func[T, R], args T) goja.Value {
promise, resolve, reject := vm.NewPromise()
go func() {
ctx := context.Background()
result, err := fn(ctx, args)
if err != nil {
_ = reject(vm.ToValue(err.Error()))
} else {
_ = resolve(convertGoValueToJS(vm, reflect.ValueOf(result)))
}
}()
return vm.ToValue(promise)
}