mirror of
https://github.com/JuLi0n21/goTsClient.git
synced 2026-04-20 00:10:07 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f35257ccc3 | |||
| a7dbfebba9 |
66
generator.go
66
generator.go
@@ -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
141
generator_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user