mirror of
https://github.com/JuLi0n21/goTsClient.git
synced 2026-04-19 16:00:07 +00:00
init
This commit is contained in:
242
generator.go
Normal file
242
generator.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package gotsclient
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed api.ts
|
||||
var backendClient string
|
||||
|
||||
var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
func GenClient(api any, outPutPath string) error {
|
||||
|
||||
ts, err := GenerateTS(reflect.TypeOf(api), "RPCClient")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(outPutPath, []byte(ts), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("Generated TS client", outPutPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateTS(apiType reflect.Type, clientName string) (string, error) {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("// --- AUTO-GENERATED ---\n")
|
||||
b.WriteString("// Generated by generator. Do not edit by hand (unless you know what you do).\n")
|
||||
b.WriteString("// Types\n\n")
|
||||
|
||||
structs := map[string]reflect.Type{}
|
||||
|
||||
for method := range apiType.Methods() {
|
||||
i := -1
|
||||
for param := range method.Type.Ins() {
|
||||
i++
|
||||
//skip self
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
log.Println(param)
|
||||
collectStructs(param, structs)
|
||||
}
|
||||
|
||||
outParams := method.Type.NumOut()
|
||||
|
||||
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) ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(structs))
|
||||
for n := range structs {
|
||||
names = append(names, n)
|
||||
}
|
||||
|
||||
slices.Sort(names)
|
||||
|
||||
for _, name := range names {
|
||||
st := structs[name]
|
||||
b.WriteString(fmt.Sprintf("export interface %s {\n", name))
|
||||
for f := range st.Fields() {
|
||||
jsName := f.Name
|
||||
tag := f.Tag.Get("json")
|
||||
if tag != "" {
|
||||
parts := strings.Split(tag, ",")
|
||||
if parts[0] != "" {
|
||||
jsName = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
optional := ""
|
||||
ft := f.Type
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
optional = "?"
|
||||
ft = ft.Elem()
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s%s: %s;\n", jsName, optional, goTypeToTS(ft)))
|
||||
}
|
||||
b.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
b.WriteString("// Generic RPC method result\n")
|
||||
b.WriteString("export type RPCResult<T> = { data: T; error?: any };\n\n")
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pname = fmt.Sprintf("arg%d", i-1)
|
||||
}
|
||||
|
||||
ptype := goTypeToTS(ptypeGo)
|
||||
|
||||
params = append(params, fmt.Sprintf("%s: %s", pname, ptype))
|
||||
}
|
||||
|
||||
resType := "any"
|
||||
if m.Type.NumOut() > 0 {
|
||||
resType = goTypeToTS(m.Type.Out(0))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s(%s): Promise<RPCResult<%s>>;\n", m.Name, strings.Join(params, ", "), resType))
|
||||
}
|
||||
b.WriteString("}\n\n")
|
||||
|
||||
b.WriteString(backendClient)
|
||||
|
||||
b.WriteString("\nlet backendWrapper: WSBackend | null = null;\n")
|
||||
b.WriteString(fmt.Sprintf("let backendInstance: %s | null = null;\n\n", clientName))
|
||||
b.WriteString(fmt.Sprintf("export function useBackend(url: string = '/ws'): { api: %s, backend: WSBackend } {\n", clientName))
|
||||
b.WriteString(" if (url.startsWith('/')) {\n")
|
||||
b.WriteString(" const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';\n")
|
||||
b.WriteString(" const host = window.location.host;\n")
|
||||
b.WriteString(" url = `${protocol}://${host}${url}`;\n")
|
||||
b.WriteString(" }\n\n")
|
||||
b.WriteString(" if (!backendWrapper) {\n")
|
||||
b.WriteString(" backendWrapper = new WSBackend(url);\n")
|
||||
b.WriteString(" backendInstance = backendWrapper.api as unknown as " + clientName + ";\n")
|
||||
b.WriteString(" }\n")
|
||||
b.WriteString(" return { api: backendInstance!, backend: backendWrapper! };\n")
|
||||
b.WriteString("}\n")
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func collectStructs(t reflect.Type, m map[string]reflect.Type) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
name := t.Name()
|
||||
if name == "" {
|
||||
// anonymous struct: skip named interface generation; will fallback to 'any' or inline handling
|
||||
return
|
||||
}
|
||||
if _, ok := m[name]; ok {
|
||||
return
|
||||
}
|
||||
m[name] = t
|
||||
|
||||
for field := range t.Fields() {
|
||||
collectStructs(field.Type, m)
|
||||
}
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
collectStructs(t.Elem(), m)
|
||||
case reflect.Map:
|
||||
collectStructs(t.Elem(), m)
|
||||
}
|
||||
}
|
||||
|
||||
func goTypeToTS(t reflect.Type) string {
|
||||
if t == nil {
|
||||
return "any"
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
return goTypeToTS(t.Elem())
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
return "number"
|
||||
case reflect.String:
|
||||
return "string"
|
||||
case reflect.Bool:
|
||||
return "boolean"
|
||||
case reflect.Struct:
|
||||
if t.PkgPath() == "time" && t.Name() == "Time" {
|
||||
return "Date"
|
||||
}
|
||||
|
||||
if t.Name() != "" {
|
||||
return t.Name()
|
||||
}
|
||||
var parts []string
|
||||
for f := range t.Fields() {
|
||||
fn := f.Name
|
||||
tag := f.Tag.Get("json")
|
||||
if tag != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", strings.Split(tag, ",")[0], goTypeToTS(f.Type)))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", fn, goTypeToTS(f.Type)))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("{ %s }", strings.Join(parts, "; "))
|
||||
case reflect.Slice, reflect.Array:
|
||||
if t.Elem().Kind() == reflect.Uint8 {
|
||||
return "string"
|
||||
}
|
||||
return goTypeToTS(t.Elem()) + "[]"
|
||||
case reflect.Map:
|
||||
key := t.Key()
|
||||
if key.Kind() == reflect.String {
|
||||
return fmt.Sprintf("{ [key: string]: %s }", goTypeToTS(t.Elem()))
|
||||
}
|
||||
return "any"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user