From f35257ccc31617ae75a8d5bc73289de09c3da2da Mon Sep 17 00:00:00 2001 From: JuLi0n21 Date: Sun, 15 Mar 2026 14:45:52 +0100 Subject: [PATCH] allow func(...) (err) back --- generator.go | 65 ++++++++++++--------- generator_test.go | 141 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 27 deletions(-) create mode 100644 generator_test.go diff --git a/generator.go b/generator.go index 5ee1788..f552ac4 100644 --- a/generator.go +++ b/generator.go @@ -49,10 +49,28 @@ func GenerateTS(apiType reflect.Type, clientName string) (string, error) { outParams := method.Type.NumOut() + 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) + } else { + return "", errors.New(method.Name + unsupportedMethod) + } + } + + //func() (struct, err) if outParams > 1 && method.Type.Out(1).Implements(errorType) { collectStructs(method.Type.Out(0), structs) - } else { - return "", errors.New("supported layout are func() (Struct, error), func(x...) (*Struct, error) ") + } + + if outParams > 2 { + return "", errors.New(method.Name + unsupportedMethod) } } @@ -94,46 +112,39 @@ func GenerateTS(apiType reflect.Type, clientName string) (string, error) { b.WriteString(fmt.Sprintf("export interface %s {\n", clientName)) for m := range apiType.Methods() { params := []string{} - i := -1 - for ptypeGo := range m.Type.Ins() { - i++ - //skip self or context - if ptypeGo.String() == "context.Context" || i == 0 { + + tsParamIdx := 0 + for i := 0; i < m.Type.NumIn(); i++ { + ptypeGo := m.Type.In(i) + + if i == 0 { + continue + } + if ptypeGo.Implements(reflect.TypeOf((*interface{ Done() <-chan struct{} })(nil)).Elem()) || + strings.Contains(ptypeGo.String(), "context.Context") { continue } var pname string - if ptypeGo.Kind() == reflect.Struct { - 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:] - } else { - pname = fmt.Sprintf("arg%d", i-1) - } - } + if ptypeGo.Kind() == reflect.Struct && ptypeGo.Name() != "" { + pname = strings.ToLower(ptypeGo.Name()[:1]) + ptypeGo.Name()[1:] } else { - pname = fmt.Sprintf("arg%d", i-1) + pname = fmt.Sprintf("arg%d", tsParamIdx) } ptype := goTypeToTS(ptypeGo) - params = append(params, fmt.Sprintf("%s: %s", pname, ptype)) + tsParamIdx++ } resType := "any" if m.Type.NumOut() > 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>;\n", m.Name, strings.Join(params, ", "), resType)) } b.WriteString("}\n\n") diff --git a/generator_test.go b/generator_test.go new file mode 100644 index 0000000..73c1625 --- /dev/null +++ b/generator_test.go @@ -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>;", + "CreateOrder(order: Order): Promise>;", + "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") + } +}