Files
poiesis/builtin.go
2026-01-27 10:08:07 -05:00

202 lines
4.2 KiB
Go

package main
import (
"fmt"
"io"
"net/http"
"reflect"
"strings"
"sync"
"github.com/dop251/goja"
)
type BuiltinFunction any
type Builtin struct {
Name string
Function any
Definition string
}
var (
builtinRegistry = make(map[string]Builtin)
registryMutex sync.RWMutex
)
func RegisterBuiltin[T any](name string, fn T) {
builtinRegistry[name] = createBuiltin(name, fn)
}
func createBuiltin(name string, fn any) Builtin {
fnValue := reflect.ValueOf(fn)
fnType := fnValue.Type()
tsDef := generateTypeScriptDefinition(name, fnType)
return Builtin{
Name: name,
Function: fn,
Definition: tsDef,
}
}
func generateTypeScriptDefinition(name string, fnType reflect.Type) string {
if fnType.Kind() != reflect.Func {
return ""
}
var params []string
for i := 0; i < fnType.NumIn(); i++ {
params = append(params, fmt.Sprintf("arg%d: %s", i, goTypeToTSType(fnType.In(i))))
}
returnSignature := "void"
if fnType.NumOut() > 0 {
returnType := fnType.Out(0)
returnSignature = goTypeToTSType(returnType)
}
return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature)
}
func goTypeToTSType(t reflect.Type) string {
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, reflect.Pointer:
if t.String() == "goja.Value" {
return "any"
}
return "any"
case reflect.Slice:
return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem()))
case reflect.Map:
return "Record<string, any>"
case reflect.Struct:
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) {
RegisterFetchBuiltin(vm)
registryMutex.RLock()
defer registryMutex.RUnlock()
for name, builtin := range builtinRegistry {
if builtin.Function != nil {
_ = vm.Set(name, builtin.Function)
}
}
}
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 {
headers[key] = values[0]
headers[strings.ToLower(key)] = values[0]
}
}
return &FetchResult{
OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
Status: resp.StatusCode,
Body: string(body),
Headers: headers,
}, nil
}
func RegisterFetchBuiltin(vm *goja.Runtime) {
_ = vm.Set("fetch", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic("fetch requires at least 1 argument")
}
url := call.Arguments[0].String()
result, err := Fetch(url, nil)
if err != nil {
panic(err)
}
resultObj := vm.NewObject()
_ = resultObj.Set("ok", result.OK)
_ = resultObj.Set("status", result.Status)
body := result.Body
_ = resultObj.Set("text", func() string {
return 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])
})
_ = resultObj.Set("headers", headersObj)
return resultObj
})
builtinRegistry["fetch"] = Builtin{
Name: "fetch",
Function: nil,
Definition: "declare function fetch(url: string, options?: any): PromiseLike<any>;",
}
}
func init() {
}