asd
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
poiesis
|
|
||||||
66
AGENTS.md
66
AGENTS.md
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with goja. Features a flexible builtin system for exposing Go functions to TypeScript.
|
Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with goja. Features a flexible builtin system for exposing Go functions to TypeScript with support for both synchronous and asynchronous (Promise-based) operations.
|
||||||
|
|
||||||
## Build & Test
|
## Build & Test
|
||||||
|
|
||||||
@@ -23,23 +23,63 @@ reichard.io/poiesis/
|
|||||||
├── cmd/poiesis/ # CLI entry point
|
├── cmd/poiesis/ # CLI entry point
|
||||||
│ └── main.go
|
│ └── main.go
|
||||||
├── internal/
|
├── internal/
|
||||||
│ └── runtime/
|
│ ├── runtime/ # Runtime management, transpilation, execution
|
||||||
│ ├── pkg/
|
│ │ ├── runtime.go
|
||||||
│ │ └── builtin/ # Builtin framework (framework only, no implementations)
|
│ │ └── runtime_test.go
|
||||||
│ │ └── builtin.go
|
│ ├── builtin/ # Builtin registration framework
|
||||||
│ ├── standard/ # Standard builtin implementations
|
│ │ ├── types.go
|
||||||
│ │ ├── fetch.go
|
│ │ ├── registry.go
|
||||||
│ │ └── fetch_test.go
|
│ │ ├── wrapper.go
|
||||||
│ ├── runtime.go # Runtime management, transpilation, execution
|
│ │ ├── convert.go
|
||||||
│ └── runtime_test.go
|
│ │ ├── typescript.go
|
||||||
|
│ │ └── builtin_test.go
|
||||||
|
│ └── standard/ # Standard builtin implementations
|
||||||
|
│ ├── fetch.go
|
||||||
|
│ ├── fetch_test.go
|
||||||
|
│ └── fetch_promise_test.go
|
||||||
└── test_data/ # Test TypeScript files
|
└── test_data/ # Test TypeScript files
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Packages
|
## Key Packages
|
||||||
|
|
||||||
- `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, execution
|
- `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, JavaScript execution
|
||||||
- `reichard.io/poiesis/internal/runtime/pkg/builtin` - Generic builtin registration framework (framework only)
|
- `reichard.io/poiesis/internal/builtin` - Generic builtin registration framework (sync/async wrappers, JS/Go conversion, type definition generation)
|
||||||
- `reichard.io/poiesis/internal/runtime/standard` - Standard builtin implementations (fetch, add, greet, etc.)
|
- `reichard.io/poiesis/internal/standard` - Standard builtin implementations (fetch, add, greet, etc.)
|
||||||
|
|
||||||
|
## Builtin System
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
|
||||||
|
Two types of builtins:
|
||||||
|
- **Sync**: `RegisterBuiltin[T, R](name, fn)` - executes synchronously, returns value
|
||||||
|
- **Async**: `RegisterAsyncBuiltin[T, R](name, fn)` - runs in goroutine, returns Promise
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Args must be a struct implementing `Args` interface with `Validate() error` method
|
||||||
|
- Use JSON tags for TypeScript type definitions
|
||||||
|
- Async builtins automatically generate `Promise<R>` return types
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AddArgs struct {
|
||||||
|
A int `json:"a"`
|
||||||
|
B int `json:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AddArgs) Validate() error { return nil }
|
||||||
|
|
||||||
|
func Add(_ context.Context, args AddArgs) (int, error) {
|
||||||
|
return args.A + args.B, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register sync builtin
|
||||||
|
builtin.RegisterBuiltin[AddArgs, int]("add", Add)
|
||||||
|
|
||||||
|
// Register async builtin
|
||||||
|
builtin.RegisterAsyncBuiltin[FetchArgs, *FetchResult]("fetch", Fetch)
|
||||||
|
```
|
||||||
|
|
||||||
## Testing Patterns
|
## Testing Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ reichard.io/poiesis/
|
|||||||
│ │ └── fetch_test.go # Tests for fetch
|
│ │ └── fetch_test.go # Tests for fetch
|
||||||
│ ├── runtime.go # Transpilation & execution
|
│ ├── runtime.go # Transpilation & execution
|
||||||
│ └── runtime_test.go # Runtime tests
|
│ └── runtime_test.go # Runtime tests
|
||||||
└── examples/ # Example TypeScript files
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -81,6 +80,7 @@ func init() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
That's it! The framework automatically:
|
That's it! The framework automatically:
|
||||||
|
|
||||||
- Converts TypeScript values to Go types
|
- Converts TypeScript values to Go types
|
||||||
- Handles errors (panics as JS errors)
|
- Handles errors (panics as JS errors)
|
||||||
- Generates TypeScript definitions
|
- Generates TypeScript definitions
|
||||||
|
|||||||
33
cmd/poiesis/main.go
Normal file
33
cmd/poiesis/main.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"reichard.io/poiesis/internal/builtin"
|
||||||
|
"reichard.io/poiesis/internal/runtime"
|
||||||
|
_ "reichard.io/poiesis/internal/standard"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage: poiesis <typescript-file>")
|
||||||
|
fmt.Fprintln(os.Stderr, " poiesis -print-types")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print Types
|
||||||
|
if os.Args[1] == "-print-types" {
|
||||||
|
fmt.Println(builtin.GetBuiltinsDeclarations())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get File
|
||||||
|
filePath := os.Args[1]
|
||||||
|
|
||||||
|
// Run File
|
||||||
|
rt := runtime.New()
|
||||||
|
if err := rt.RunFile(filePath, os.Stdout, os.Stderr); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// Example: How to add builtins to the framework
|
|
||||||
// Just write a Go function and register it - that's all!
|
|
||||||
|
|
||||||
package standard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"reichard.io/poiesis/internal/runtime/pkg/builtin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Simple function - just register it!
|
|
||||||
func multiply(a, b int) int {
|
|
||||||
return a * b
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function returning multiple values with error
|
|
||||||
func divide(a, b int) (int, error) {
|
|
||||||
if b == 0 {
|
|
||||||
return 0, fmt.Errorf("cannot divide by zero")
|
|
||||||
}
|
|
||||||
return a / b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complex example with struct
|
|
||||||
type User struct {
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
Age int
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUser(id int) (User, error) {
|
|
||||||
return User{
|
|
||||||
Name: "John Doe",
|
|
||||||
Email: "john@example.com",
|
|
||||||
Age: 30,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: Register custom converter for User type
|
|
||||||
func convertUser(vm *goja.Runtime, user User) goja.Value {
|
|
||||||
obj := vm.NewObject()
|
|
||||||
_ = obj.Set("name", user.Name)
|
|
||||||
_ = obj.Set("email", user.Email)
|
|
||||||
_ = obj.Set("age", user.Age)
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
// In a real file, you'd put this in init():
|
|
||||||
//
|
|
||||||
// func init() {
|
|
||||||
// builtin.RegisterCustomConverter(convertUser)
|
|
||||||
// builtin.RegisterBuiltin("multiply", multiply)
|
|
||||||
// builtin.RegisterBuiltin("divide", divide)
|
|
||||||
// builtin.RegisterBuiltin("getUser", getUser)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// That's it! TypeScript definitions are auto-generated:
|
|
||||||
// declare function multiply(arg0: number, arg1: number): number;
|
|
||||||
// declare function divide(arg0: number, arg1: number): number;
|
|
||||||
// declare function getUser(arg0: number): User;
|
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
go
|
go
|
||||||
gopls
|
gopls
|
||||||
golangci-lint
|
golangci-lint
|
||||||
|
|
||||||
|
tree
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
77
internal/builtin/builtin_test.go
Normal file
77
internal/builtin/builtin_test.go
Normal 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())
|
||||||
|
}
|
||||||
170
internal/builtin/collector.go
Normal file
170
internal/builtin/collector.go
Normal 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
204
internal/builtin/convert.go
Normal 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
|
||||||
|
}
|
||||||
90
internal/builtin/registry.go
Normal file
90
internal/builtin/registry.go
Normal 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
27
internal/builtin/types.go
Normal 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)
|
||||||
64
internal/builtin/typescript.go
Normal file
64
internal/builtin/typescript.go
Normal 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)
|
||||||
|
}
|
||||||
273
internal/builtin/typescript_test.go
Normal file
273
internal/builtin/typescript_test.go
Normal 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)
|
||||||
|
}
|
||||||
72
internal/builtin/wrapper.go
Normal file
72
internal/builtin/wrapper.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/evanw/esbuild/pkg/api"
|
"github.com/evanw/esbuild/pkg/api"
|
||||||
"reichard.io/poiesis/internal/runtime/pkg/builtin"
|
"reichard.io/poiesis/internal/builtin"
|
||||||
_ "reichard.io/poiesis/internal/runtime/standard"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Runtime struct {
|
type Runtime struct {
|
||||||
@@ -18,12 +17,12 @@ type Runtime struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() *Runtime {
|
func New() *Runtime {
|
||||||
vm := goja.New()
|
// Create Runtime
|
||||||
|
r := &Runtime{vm: goja.New(), stdout: os.Stdout, stderr: os.Stderr}
|
||||||
r := &Runtime{vm: vm, stdout: os.Stdout, stderr: os.Stderr}
|
|
||||||
r.setupConsole()
|
r.setupConsole()
|
||||||
builtin.RegisterBuiltins(vm)
|
|
||||||
|
|
||||||
|
// Register Builtins
|
||||||
|
builtin.RegisterBuiltins(r.vm)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
_ "reichard.io/poiesis/internal/standard"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,34 +1,39 @@
|
|||||||
package standard
|
package standard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"reichard.io/poiesis/internal/runtime/pkg/builtin"
|
"reichard.io/poiesis/internal/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FetchArgs struct {
|
type FetchArgs struct {
|
||||||
Input string `json:"input"`
|
Input string `json:"input"`
|
||||||
Init *FetchOptions `json:"init,omitempty"`
|
Init *RequestInit `json:"init,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetchOptions struct {
|
func (f FetchArgs) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestInit struct {
|
||||||
Method string `json:"method,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
Headers map[string]string `json:"headers,omitempty"`
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
Body *string `json:"body,omitempty"`
|
Body *string `json:"body,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *FetchOptions) Defaults() *FetchOptions {
|
func (o *RequestInit) Validate() error {
|
||||||
if o.Method == "" {
|
if o.Method == "" {
|
||||||
o.Method = "GET"
|
o.Method = "GET"
|
||||||
}
|
}
|
||||||
return o
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetchResult struct {
|
type Response struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body"`
|
||||||
@@ -40,11 +45,19 @@ type AddArgs struct {
|
|||||||
B int `json:"b"`
|
B int `json:"b"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AddArgs) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type GreetArgs struct {
|
type GreetArgs struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Fetch(args FetchArgs) (*FetchResult, error) {
|
func (g GreetArgs) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fetch(_ context.Context, args FetchArgs) (Response, error) {
|
||||||
method := "GET"
|
method := "GET"
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
|
|
||||||
@@ -57,7 +70,7 @@ func Fetch(args FetchArgs) (*FetchResult, error) {
|
|||||||
|
|
||||||
req, err := http.NewRequest(method, args.Input, nil)
|
req, err := http.NewRequest(method, args.Input, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
return Response{}, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range headers {
|
for k, v := range headers {
|
||||||
@@ -66,7 +79,7 @@ func Fetch(args FetchArgs) (*FetchResult, error) {
|
|||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch: %w", err)
|
return Response{}, fmt.Errorf("failed to fetch: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
@@ -74,7 +87,7 @@ func Fetch(args FetchArgs) (*FetchResult, error) {
|
|||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read body: %w", err)
|
return Response{}, fmt.Errorf("failed to read body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resultHeaders := make(map[string]string)
|
resultHeaders := make(map[string]string)
|
||||||
@@ -86,7 +99,7 @@ func Fetch(args FetchArgs) (*FetchResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &FetchResult{
|
return Response{
|
||||||
OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
|
OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||||
Status: resp.StatusCode,
|
Status: resp.StatusCode,
|
||||||
Body: string(body),
|
Body: string(body),
|
||||||
@@ -94,16 +107,16 @@ func Fetch(args FetchArgs) (*FetchResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(args AddArgs) int {
|
func Add(_ context.Context, args AddArgs) (int, error) {
|
||||||
return args.A + args.B
|
return args.A + args.B, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func greet(args GreetArgs) string {
|
func Greet(_ context.Context, args GreetArgs) (string, error) {
|
||||||
return fmt.Sprintf("Hello, %s!", args.Name)
|
return fmt.Sprintf("Hello, %s!", args.Name), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
builtin.RegisterBuiltin[FetchArgs, *FetchResult]("fetch", Fetch, builtin.WithPromise())
|
builtin.RegisterAsyncBuiltin("fetch", Fetch)
|
||||||
builtin.RegisterBuiltin[AddArgs, int]("add", add)
|
builtin.RegisterBuiltin("add", Add)
|
||||||
builtin.RegisterBuiltin[GreetArgs, string]("greet", greet)
|
builtin.RegisterBuiltin("greet", Greet)
|
||||||
}
|
}
|
||||||
51
internal/standard/fetch_promise_test.go
Normal file
51
internal/standard/fetch_promise_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
|
||||||
|
"reichard.io/poiesis/internal/builtin"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetchReturnsPromise(t *testing.T) {
|
||||||
|
vm := goja.New()
|
||||||
|
builtin.RegisterBuiltins(vm)
|
||||||
|
|
||||||
|
result, err := vm.RunString(`fetch({input: "https://example.com"})`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
promise, ok := result.Export().(*goja.Promise)
|
||||||
|
require.True(t, ok, "fetch should return a Promise")
|
||||||
|
assert.NotNil(t, promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchAsyncAwait(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
builtin.RegisterBuiltins(vm)
|
||||||
|
|
||||||
|
result, err := vm.RunString(`
|
||||||
|
async function testFetch() {
|
||||||
|
const response = await fetch({input: "` + server.URL + `"});
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
testFetch();
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
promise, ok := result.Export().(*goja.Promise)
|
||||||
|
require.True(t, ok, "async function should return a Promise")
|
||||||
|
assert.NotNil(t, promise)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package standard
|
package standard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFetch(t *testing.T) {
|
func TestFetch(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("X-Custom-Header", "test-value")
|
w.Header().Set("X-Custom-Header", "test-value")
|
||||||
@@ -18,7 +20,7 @@ func TestFetch(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := Fetch(FetchArgs{Input: server.URL})
|
result, err := Fetch(ctx, FetchArgs{Input: server.URL})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, result.OK)
|
assert.True(t, result.OK)
|
||||||
@@ -31,8 +33,9 @@ func TestFetch(t *testing.T) {
|
|||||||
|
|
||||||
func TestFetchHTTPBin(t *testing.T) {
|
func TestFetchHTTPBin(t *testing.T) {
|
||||||
t.Skip("httpbin.org test is flaky")
|
t.Skip("httpbin.org test is flaky")
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
result, err := Fetch(FetchArgs{Input: "https://httpbin.org/get"})
|
result, err := Fetch(ctx, FetchArgs{Input: "https://httpbin.org/get"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, result.OK)
|
assert.True(t, result.OK)
|
||||||
@@ -42,7 +45,8 @@ func TestFetchHTTPBin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchWith404(t *testing.T) {
|
func TestFetchWith404(t *testing.T) {
|
||||||
result, err := Fetch(FetchArgs{Input: "https://httpbin.org/status/404"})
|
ctx := context.Background()
|
||||||
|
result, err := Fetch(ctx, FetchArgs{Input: "https://httpbin.org/status/404"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.False(t, result.OK)
|
assert.False(t, result.OK)
|
||||||
@@ -50,12 +54,14 @@ func TestFetchWith404(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchWithInvalidURL(t *testing.T) {
|
func TestFetchWithInvalidURL(t *testing.T) {
|
||||||
_, err := Fetch(FetchArgs{Input: "http://this-domain-does-not-exist-12345.com"})
|
ctx := context.Background()
|
||||||
|
_, err := Fetch(ctx, FetchArgs{Input: "http://this-domain-does-not-exist-12345.com"})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "failed to fetch")
|
assert.Contains(t, err.Error(), "failed to fetch")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchWithHeaders(t *testing.T) {
|
func TestFetchWithHeaders(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
|
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
|
||||||
assert.Equal(t, "GET", r.Method)
|
assert.Equal(t, "GET", r.Method)
|
||||||
@@ -67,16 +73,17 @@ func TestFetchWithHeaders(t *testing.T) {
|
|||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
"Authorization": "Bearer test-token",
|
"Authorization": "Bearer test-token",
|
||||||
}
|
}
|
||||||
options := &FetchOptions{
|
options := &RequestInit{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Headers: headers,
|
Headers: headers,
|
||||||
}
|
}
|
||||||
result, err := Fetch(FetchArgs{Input: server.URL, Init: options})
|
result, err := Fetch(ctx, FetchArgs{Input: server.URL, Init: options})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, result.OK)
|
assert.True(t, result.OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchDefaults(t *testing.T) {
|
func TestFetchDefaults(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
assert.Equal(t, "GET", r.Method, "default method should be GET")
|
assert.Equal(t, "GET", r.Method, "default method should be GET")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -84,24 +91,30 @@ func TestFetchDefaults(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
options := &FetchOptions{}
|
options := &RequestInit{}
|
||||||
result, err := Fetch(FetchArgs{Input: server.URL, Init: options})
|
result, err := Fetch(ctx, FetchArgs{Input: server.URL, Init: options})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, result.OK)
|
assert.True(t, result.OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdd(t *testing.T) {
|
func TestAdd(t *testing.T) {
|
||||||
result := add(AddArgs{A: 5, B: 10})
|
ctx := context.Background()
|
||||||
|
result, err := Add(ctx, AddArgs{A: 5, B: 10})
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 15, result)
|
assert.Equal(t, 15, result)
|
||||||
|
|
||||||
result = add(AddArgs{A: -3, B: 7})
|
result, err = Add(ctx, AddArgs{A: -3, B: 7})
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 4, result)
|
assert.Equal(t, 4, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGreet(t *testing.T) {
|
func TestGreet(t *testing.T) {
|
||||||
result := greet(GreetArgs{Name: "World"})
|
ctx := context.Background()
|
||||||
|
result, err := Greet(ctx, GreetArgs{Name: "World"})
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "Hello, World!", result)
|
assert.Equal(t, "Hello, World!", result)
|
||||||
|
|
||||||
result = greet(GreetArgs{Name: "Alice"})
|
result, err = Greet(ctx, GreetArgs{Name: "Alice"})
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "Hello, Alice!", result)
|
assert.Equal(t, "Hello, Alice!", result)
|
||||||
}
|
}
|
||||||
11
test_data/fetch_promise_test.ts
Normal file
11
test_data/fetch_promise_test.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
async function logPromiseResult() {
|
||||||
|
try {
|
||||||
|
const response = await fetch({input: "https://httpbin.org/get"});
|
||||||
|
console.log("Fetch successful, OK:", response.ok);
|
||||||
|
console.log("Status:", response.status);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Fetch failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logPromiseResult();
|
||||||
Reference in New Issue
Block a user