This commit is contained in:
2026-01-27 10:37:40 -05:00
parent 60fb12e52c
commit f039a12a66
7 changed files with 209 additions and 112 deletions

View File

@@ -2,8 +2,6 @@ package builtin
import (
"fmt"
"io"
"net/http"
"reflect"
"strings"
"sync"
@@ -18,8 +16,9 @@ type Builtin struct {
}
var (
builtinRegistry = make(map[string]Builtin)
registryMutex sync.RWMutex
builtinRegistry = make(map[string]Builtin)
registryMutex sync.RWMutex
customConverters = make(map[reflect.Type]func(*goja.Runtime, reflect.Value) goja.Value)
)
func RegisterBuiltin(name string, fn any) {
@@ -29,11 +28,35 @@ func RegisterBuiltin(name string, fn any) {
wrapper := createGenericWrapper(fnValue, fnType)
definition := generateTypeScriptDefinition(name, fnType)
registryMutex.Lock()
builtinRegistry[name] = Builtin{
Name: name,
Function: wrapper,
Definition: definition,
}
registryMutex.Unlock()
}
func RegisterCustomConverter[T any](converter func(vm *goja.Runtime, value T) goja.Value) {
var t T
typeOf := reflect.TypeOf(t)
registryMutex.Lock()
wrappedConverter := func(vm *goja.Runtime, value reflect.Value) goja.Value {
return converter(vm, value.Interface().(T))
}
customConverters[typeOf] = wrappedConverter
if typeOf.Kind() == reflect.Pointer {
elemType := typeOf.Elem()
customConverters[elemType] = func(vm *goja.Runtime, value reflect.Value) goja.Value {
if value.IsNil() {
return goja.Null()
}
return converter(vm, value.Interface().(T))
}
}
registryMutex.Unlock()
}
func createGenericWrapper(fnValue reflect.Value, fnType reflect.Type) any {
@@ -152,6 +175,26 @@ func convertJSValueToGo(vm *goja.Runtime, jsValue goja.Value, targetType reflect
func convertGoValueToJS(vm *goja.Runtime, goValue reflect.Value) goja.Value {
value := goValue.Interface()
valueType := goValue.Type()
registryMutex.RLock()
converter, ok := customConverters[valueType]
registryMutex.RUnlock()
if ok {
return converter(vm, goValue)
}
if goValue.Kind() == reflect.Pointer && !goValue.IsNil() {
elemType := goValue.Type().Elem()
registryMutex.RLock()
converter, ok := customConverters[elemType]
registryMutex.RUnlock()
if ok {
return converter(vm, goValue.Elem())
}
}
switch v := value.(type) {
case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool:
@@ -181,33 +224,6 @@ func convertGoValueToJS(vm *goja.Runtime, goValue reflect.Value) goja.Value {
}
return vm.ToValue(arr)
case FetchResult:
obj := vm.NewObject()
_ = obj.Set("ok", v.OK)
_ = obj.Set("status", v.Status)
_ = obj.Set("text", func() string {
return v.Body
})
headersObj := vm.NewObject()
headers := v.Headers
_ = headersObj.Set("get", func(c goja.FunctionCall) goja.Value {
if len(c.Arguments) < 1 {
return goja.Undefined()
}
key := c.Arguments[0].String()
return vm.ToValue(headers[key])
})
_ = obj.Set("headers", headersObj)
return obj
case *FetchResult:
if v == nil {
return goja.Null()
}
return convertGoValueToJS(vm, reflect.ValueOf(*v))
default:
return vm.ToValue(v)
}
@@ -280,9 +296,6 @@ func goTypeToTSType(t reflect.Type) string {
}
return "Record<string, any>"
case reflect.Struct:
if t.Name() == "FetchResult" {
return "Response"
}
return "any"
default:
return "any"
@@ -312,56 +325,3 @@ func RegisterBuiltins(vm *goja.Runtime) {
}
}
}
type FetchResult struct {
OK bool
Status int
Body string
Headers map[string]string
}
func Fetch(url string, options map[string]any) (*FetchResult, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read body: %w", err)
}
headers := make(map[string]string)
for key, values := range resp.Header {
if len(values) > 0 {
val := values[0]
headers[key] = val
headers[strings.ToLower(key)] = val
}
}
return &FetchResult{
OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
Status: resp.StatusCode,
Body: string(body),
Headers: headers,
}, nil
}
func init() {
RegisterBuiltin("fetch", Fetch)
RegisterBuiltin("add", func(a, b int) int {
return a + b
})
RegisterBuiltin("greet", func(name string) string {
return fmt.Sprintf("Hello, %s!", name)
})
}

View File

@@ -7,7 +7,8 @@ import (
"github.com/dop251/goja"
"github.com/evanw/esbuild/pkg/api"
"reichard.io/poiesis/internal/builtin"
"reichard.io/poiesis/internal/runtime/pkg/builtin"
_ "reichard.io/poiesis/internal/runtime/standard"
)
type Runtime struct {

View File

@@ -0,0 +1,92 @@
package standard
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/dop251/goja"
"reichard.io/poiesis/internal/runtime/pkg/builtin"
)
type FetchResult struct {
OK bool
Status int
Body string
Headers map[string]string
}
func Fetch(url string, options map[string]any) (*FetchResult, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read body: %w", err)
}
headers := make(map[string]string)
for key, values := range resp.Header {
if len(values) > 0 {
val := values[0]
headers[key] = val
headers[strings.ToLower(key)] = val
}
}
return &FetchResult{
OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
Status: resp.StatusCode,
Body: string(body),
Headers: headers,
}, nil
}
func convertFetchResult(vm *goja.Runtime, result *FetchResult) goja.Value {
if result == nil {
return goja.Null()
}
obj := vm.NewObject()
_ = obj.Set("ok", result.OK)
_ = obj.Set("status", result.Status)
_ = obj.Set("text", func() string {
return result.Body
})
headersObj := vm.NewObject()
headers := result.Headers
_ = headersObj.Set("get", func(c goja.FunctionCall) goja.Value {
if len(c.Arguments) < 1 {
return goja.Undefined()
}
key := c.Arguments[0].String()
return vm.ToValue(headers[key])
})
_ = obj.Set("headers", headersObj)
return obj
}
func init() {
builtin.RegisterCustomConverter(convertFetchResult)
builtin.RegisterBuiltin("fetch", Fetch)
builtin.RegisterBuiltin("add", func(a, b int) int {
return a + b
})
builtin.RegisterBuiltin("greet", func(name string) string {
return fmt.Sprintf("Hello, %s!", name)
})
}

View File

@@ -1,4 +1,4 @@
package builtin
package standard
import (
"net/http"
@@ -30,6 +30,8 @@ func TestFetch(t *testing.T) {
}
func TestFetchHTTPBin(t *testing.T) {
t.Skip("httpbin.org test is flaky")
result, err := Fetch("https://httpbin.org/get", nil)
require.NoError(t, err)