Files
poiesis/internal/runtime/pkg/builtin/builtin.go
2026-01-27 13:57:01 -05:00

464 lines
11 KiB
Go

package builtin
import (
"fmt"
"reflect"
"strings"
"sync"
"github.com/dop251/goja"
)
type Builtin struct {
Name string
Function func(*goja.Runtime) func(goja.FunctionCall) goja.Value
Definition string
isPromise bool
}
type EmptyArgs struct{}
type RegisterOption func(*Builtin) error
func WithPromise() RegisterOption {
return func(b *Builtin) error {
b.isPromise = true
return nil
}
}
var (
builtinRegistry = make(map[string]Builtin)
registryMutex sync.RWMutex
)
func RegisterBuiltin[T any, R any](name string, fn any, opts ...RegisterOption) {
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)
isPromise := false
for _, opt := range opts {
if opt != nil {
isPromise = true
break
}
}
wrapper := createWrapper[T](fn, fnType, isPromise)
registryMutex.Lock()
b := Builtin{
Name: name,
Function: wrapper,
Definition: generateTypeScriptDefinition(name, tType, fnType, isPromise),
}
for _, opt := range opts {
if opt != nil {
if err := opt(&b); err != nil {
panic(fmt.Sprintf("builtin %s: option error: %v", name, err))
}
}
}
builtinRegistry[name] = b
registryMutex.Unlock()
}
func createWrapper[T any](fn any, fnType reflect.Type, isPromise 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 defaults, ok := any(args).(interface{ Defaults() T }); ok {
args = defaults.Defaults()
}
fnValue := reflect.ValueOf(fn)
firstParamType := fnType.In(0)
argValue := reflect.ValueOf(args).Convert(firstParamType)
results := fnValue.Call([]reflect.Value{argValue})
if len(results) == 0 {
return goja.Undefined()
}
if err, isError := results[len(results)-1].Interface().(error); isError {
if err != nil {
if isPromise {
return createRejectedPromise(vm, err)
}
panic(err)
}
if len(results) == 1 {
if isPromise {
return createResolvedPromise(vm)
}
return goja.Undefined()
}
if isPromise {
return createResolvedPromise(vm, results[0])
}
return convertGoValueToJS(vm, results[0])
}
if isPromise {
return createResolvedPromise(vm, results[0])
}
return convertGoValueToJS(vm, results[0])
}
}
}
func createResolvedPromise(vm *goja.Runtime, value ...reflect.Value) goja.Value {
promise, resolve, _ := vm.NewPromise()
go func() {
if len(value) > 0 {
jsValue := convertGoValueToJS(vm, value[0])
_ = resolve(jsValue)
} else {
_ = resolve(goja.Undefined())
}
}()
return vm.ToValue(promise)
}
func createRejectedPromise(vm *goja.Runtime, err error) goja.Value {
promise, _, reject := vm.NewPromise()
go func() {
_ = reject(vm.ToValue(err.Error()))
}()
return vm.ToValue(promise)
}
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 {
// goja.Value was zero - treat as undefined
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
}
func generateTypeScriptDefinition(name string, argsType reflect.Type, fnType reflect.Type, isPromise 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
tsType := goTypeToTSType(goType, goType.Kind() == reflect.Pointer)
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 {
returnSignature = goTypeToTSType(fnType.Out(0), false)
}
} else {
returnSignature = goTypeToTSType(lastType, false)
}
}
if isPromise {
returnSignature = fmt.Sprintf("Promise<%s>", returnSignature)
}
return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature)
}
func goTypeToTSType(t reflect.Type, isPointer bool) string {
if isPointer {
if t.Kind() == reflect.Pointer {
return goTypeToTSType(t.Elem(), false) + " | null"
}
return goTypeToTSType(t, false) + " | null"
}
switch t.Kind() {
case reflect.String:
return "string"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return "number"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "number"
case reflect.Float32, reflect.Float64:
return "number"
case reflect.Bool:
return "boolean"
case reflect.Interface:
return "any"
case reflect.Slice:
return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem(), false))
case reflect.Map:
if t.Key().Kind() == reflect.String && t.Elem().Kind() == reflect.Interface {
return "Record<string, any>"
}
return "Record<string, any>"
case reflect.Struct:
fields := make([]string, 0, t.NumField())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
name := getFieldName(field)
tsType := goTypeToTSType(field.Type, field.Type.Kind() == reflect.Pointer)
if field.Type.Kind() == reflect.Pointer {
tsType = strings.TrimSuffix(tsType, " | null")
tsType += "?"
} else if strings.Contains(field.Tag.Get("json"), ",omitempty") {
tsType += "?"
}
fields = append(fields, fmt.Sprintf("%s: %s", name, tsType))
}
return fmt.Sprintf("{ %s }", strings.Join(fields, "; "))
case reflect.Pointer:
if t.Elem().Kind() == reflect.Struct {
return goTypeToTSType(t.Elem(), false)
}
return "any"
default:
return "any"
}
}
func GetBuiltinsDeclarations() string {
registryMutex.RLock()
defer registryMutex.RUnlock()
var decls []string
for _, builtin := range builtinRegistry {
decls = append(decls, builtin.Definition)
}
return strings.Join(decls, "\n")
}
func RegisterBuiltins(vm *goja.Runtime) {
registryMutex.RLock()
defer registryMutex.RUnlock()
for name, builtin := range builtinRegistry {
_ = vm.Set(name, builtin.Function(vm))
}
}