package rpc 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 = { 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>;\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" } }