2 Commits

Author SHA1 Message Date
f35257ccc3 allow func(...) (err) back 2026-03-15 14:45:52 +01:00
a7dbfebba9 remove log when generating client 2026-03-13 18:21:13 +01:00
2 changed files with 179 additions and 30 deletions

View File

@@ -4,7 +4,6 @@ import (
_ "embed" _ "embed"
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"reflect" "reflect"
"slices" "slices"
@@ -25,7 +24,6 @@ func GenClient(api any, outPutPath string) error {
if err := os.WriteFile(outPutPath, []byte(ts), 0644); err != nil { if err := os.WriteFile(outPutPath, []byte(ts), 0644); err != nil {
return err return err
} }
log.Println("Generated TS client", outPutPath)
return nil return nil
} }
@@ -46,16 +44,33 @@ func GenerateTS(apiType reflect.Type, clientName string) (string, error) {
if i == 0 { if i == 0 {
continue continue
} }
log.Println(param)
collectStructs(param, structs) collectStructs(param, structs)
} }
outParams := method.Type.NumOut() outParams := method.Type.NumOut()
if outParams > 1 && method.Type.Out(1).Implements(errorType) { unsupportedMethod := "not supported, allowed layout are \nfunc() (error) \nfunc() (struct, error), \nfunc(x...) (struct, error)"
//func()
if outParams == 0 {
return "", errors.New(method.Name + unsupportedMethod)
}
//func() (err)
if outParams == 1 {
if method.Type.Out(0).Implements(errorType) {
collectStructs(method.Type.Out(0), structs) collectStructs(method.Type.Out(0), structs)
} else { } else {
return "", errors.New("supported layout are func() (Struct, error), func(x...) (*Struct, error) ") return "", errors.New(method.Name + unsupportedMethod)
}
}
//func() (struct, err)
if outParams > 1 && method.Type.Out(1).Implements(errorType) {
collectStructs(method.Type.Out(0), structs)
}
if outParams > 2 {
return "", errors.New(method.Name + unsupportedMethod)
} }
} }
@@ -97,46 +112,39 @@ func GenerateTS(apiType reflect.Type, clientName string) (string, error) {
b.WriteString(fmt.Sprintf("export interface %s {\n", clientName)) b.WriteString(fmt.Sprintf("export interface %s {\n", clientName))
for m := range apiType.Methods() { for m := range apiType.Methods() {
params := []string{} params := []string{}
i := -1
for ptypeGo := range m.Type.Ins() { tsParamIdx := 0
i++ for i := 0; i < m.Type.NumIn(); i++ {
//skip self or context ptypeGo := m.Type.In(i)
if ptypeGo.String() == "context.Context" || i == 0 {
if i == 0 {
continue
}
if ptypeGo.Implements(reflect.TypeOf((*interface{ Done() <-chan struct{} })(nil)).Elem()) ||
strings.Contains(ptypeGo.String(), "context.Context") {
continue continue
} }
var pname string var pname string
if ptypeGo.Kind() == reflect.Struct { if ptypeGo.Kind() == reflect.Struct && ptypeGo.Name() != "" {
named := false
for fields := range ptypeGo.Fields() {
tag := fields.Tag.Get("json")
if tag != "" && tag != "-" {
pname = tag
named = true
break
}
}
if !named {
// fallback to camelCase struct name
if ptypeGo.Name() != "" {
pname = strings.ToLower(ptypeGo.Name()[:1]) + ptypeGo.Name()[1:] pname = strings.ToLower(ptypeGo.Name()[:1]) + ptypeGo.Name()[1:]
} else { } else {
pname = fmt.Sprintf("arg%d", i-1) pname = fmt.Sprintf("arg%d", tsParamIdx)
}
}
} else {
pname = fmt.Sprintf("arg%d", i-1)
} }
ptype := goTypeToTS(ptypeGo) ptype := goTypeToTS(ptypeGo)
params = append(params, fmt.Sprintf("%s: %s", pname, ptype)) params = append(params, fmt.Sprintf("%s: %s", pname, ptype))
tsParamIdx++
} }
resType := "any" resType := "any"
if m.Type.NumOut() > 0 { if m.Type.NumOut() > 0 {
resType = goTypeToTS(m.Type.Out(0)) resType = goTypeToTS(m.Type.Out(0))
if m.Type.Out(0).Implements(errorType) {
resType = "any" // It's just a func() error
} }
}
b.WriteString(fmt.Sprintf(" %s(%s): Promise<RPCResult<%s>>;\n", m.Name, strings.Join(params, ", "), resType)) b.WriteString(fmt.Sprintf(" %s(%s): Promise<RPCResult<%s>>;\n", m.Name, strings.Join(params, ", "), resType))
} }
b.WriteString("}\n\n") b.WriteString("}\n\n")

141
generator_test.go Normal file
View File

@@ -0,0 +1,141 @@
package rpc
import (
"os"
"reflect"
"strings"
"testing"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
IsActive bool `json:"is_active"`
PtrField *int `json:"ptr_field"`
}
type Order struct {
OrderID int `json:"order_id"`
Items []Item `json:"items"`
}
type Item struct {
Name string `json:"name"`
}
type MockAPI struct{}
func (s *MockAPI) GetUser(id int) (User, error) { return User{}, nil }
func (s *MockAPI) CreateOrder(o Order) error { return nil }
func (s *MockAPI) NoArgs() error { return nil }
func TestGoTypeToTS(t *testing.T) {
tests := []struct {
name string
input any
expected string
}{
{"Int", 42, "number"},
{"String", "hello", "string"},
{"Bool", true, "boolean"},
{"Slice", []string{}, "string[]"},
{"Byte Slice", []byte{}, "string"},
{"Pointer", new(int), "number"},
{"Map", map[string]int{}, "{ [key: string]: number }"},
{"Struct", User{}, "User"},
{"Anon Struct", struct {
Name string `json:"name"`
}{Name: "test"}, "{ name: string }"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
typ := reflect.TypeOf(tt.input)
got := goTypeToTS(typ)
if got != tt.expected {
t.Errorf("goTypeToTS() = %v, want %v", got, tt.expected)
}
})
}
}
func TestCollectStructs(t *testing.T) {
structs := make(map[string]reflect.Type)
// Test nested collection
collectStructs(reflect.TypeOf(Order{}), structs)
if _, ok := structs["Order"]; !ok {
t.Error("Expected Order in map")
}
if _, ok := structs["Item"]; !ok {
t.Error("Expected Item (nested) in map")
}
}
func TestGenerateTS_Success(t *testing.T) {
api := &MockAPI{}
output, err := GenerateTS(reflect.TypeOf(api), "TestClient")
if err != nil {
t.Fatalf("GenerateTS failed: %v", err)
}
expectedStrings := []string{
"export interface User {",
"id: number;",
"is_active: boolean;",
"export interface TestClient {",
"GetUser(arg0: number): Promise<RPCResult<User>>;",
"CreateOrder(order: Order): Promise<RPCResult<any>>;",
"useBackend(url: string = '/ws')",
}
for _, s := range expectedStrings {
if !strings.Contains(output, s) {
t.Errorf("Generated TS missing expected string: %s", s)
}
}
}
type APIWithTooManyReturns struct{}
func (s *APIWithTooManyReturns) Bad(id int) (int, int, error) { return 0, 0, nil }
type APIWithNoErrorHandler struct{}
func (s *APIWithNoErrorHandler) Bad(id int) int { return 0 }
func TestGenerateTS_Validation(t *testing.T) {
type BadAPI struct{}
t.Run("InvalidReturnCount", func(t *testing.T) {
api := &APIWithTooManyReturns{}
_, err := GenerateTS(reflect.TypeOf(api), "Client")
if err == nil {
t.Error("Expected error for method with 3 return values")
}
})
t.Run("NoResidentError", func(t *testing.T) {
api := &APIWithNoErrorHandler{}
_, err := GenerateTS(reflect.TypeOf(api), "Client")
if err == nil {
t.Error("Expected error for method missing 'error' return type")
}
})
}
func TestGenClient(t *testing.T) {
tmpFile := "test_client.ts"
defer os.Remove(tmpFile)
api := &MockAPI{}
err := GenClient(api, tmpFile)
if err != nil {
t.Fatalf("GenClient failed: %v", err)
}
if _, err := os.Stat(tmpFile); os.IsNotExist(err) {
t.Error("GenClient did not create the file")
}
}