// genprotocol generates the WebSocket protocol message-type constant files // for both the Go server and the TypeScript client from the single source of // truth at docs/protocol-schema.json. // // Usage (from the Server/ directory): // // go run ./scripts/genprotocol // // or via make: // // make protocol-generate # regenerate both outputs // make protocol-verify # fail if committed outputs are stale (CI) package main import ( "encoding/json" "flag" "fmt" "go/format" "log" "os" "strings" ) type message struct { Wire string `json:"wire"` Go string `json:"go"` TS string `json:"ts"` Note string `json:"note,omitempty"` GoTrailingComment string `json:"go_trailing_comment,omitempty"` } type schema struct { Comment string `json:"$comment"` Version int `json:"version"` ClientToServer []message `json:"client_to_server"` ServerToClient []message `json:"server_to_client"` } func main() { schemaPath := flag.String("schema", "../docs/protocol-schema.json", "path to protocol-schema.json") goOut := flag.String("go-out", "ws/message_types.go", "path to the generated Go file") tsOut := flag.String("ts-out", "../Client/tauri-client/src/lib/protocolTypes.ts", "path to the generated TypeScript file") flag.Parse() raw, err := os.ReadFile(*schemaPath) if err != nil { log.Fatalf("read schema: %v", err) } var s schema dec := json.NewDecoder(strings.NewReader(string(raw))) dec.DisallowUnknownFields() if err := dec.Decode(&s); err != nil { log.Fatalf("parse schema: %v", err) } if err := validate(s); err != nil { log.Fatalf("invalid schema: %v", err) } goSrc, err := renderGo(s) if err != nil { log.Fatalf("render Go: %v", err) } if err := os.WriteFile(*goOut, goSrc, 0o644); err != nil { log.Fatalf("write %s: %v", *goOut, err) } if err := os.WriteFile(*tsOut, []byte(renderTS(s)), 0o644); err != nil { log.Fatalf("write %s: %v", *tsOut, err) } fmt.Printf("generated %s (%d client→server, %d server→client) and %s\n", *goOut, len(s.ClientToServer), len(s.ServerToClient), *tsOut) } // validate rejects duplicate identifiers and empty fields early so a bad // schema edit fails the generator instead of producing broken output. func validate(s schema) error { goNames := map[string]bool{} for _, list := range [][]message{s.ClientToServer, s.ServerToClient} { tsNames := map[string]bool{} for _, m := range list { if m.Wire == "" || m.Go == "" || m.TS == "" { return fmt.Errorf("entry %+v: wire, go, and ts are all required", m) } if goNames[m.Go] { return fmt.Errorf("duplicate Go constant %q", m.Go) } goNames[m.Go] = true if tsNames[m.TS] { return fmt.Errorf("duplicate TS key %q within one direction", m.TS) } tsNames[m.TS] = true } } return nil } func header(comment string) string { return "// Code generated by scripts/genprotocol from docs/protocol-schema.json; DO NOT EDIT.\n" + comment + "\n" } func renderGo(s schema) ([]byte, error) { var b strings.Builder b.WriteString(header("//\n// WebSocket protocol message type constants — single source of truth for\n" + "// both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json\n" + "// and run `make protocol-generate` (see Server/Makefile).")) b.WriteString("\npackage ws\n\n") writeBlock := func(title string, msgs []message) { b.WriteString("// " + title + "\nconst (\n") for _, m := range msgs { b.WriteString("\t" + m.Go + " = " + fmt.Sprintf("%q", m.Wire)) switch { case m.GoTrailingComment != "": b.WriteString(" " + m.GoTrailingComment) case m.Note != "": b.WriteString(" // " + m.Note) } b.WriteString("\n") } b.WriteString(")\n") } writeBlock("Client → Server message types (received by handlers).", s.ClientToServer) b.WriteString("\n") writeBlock("Server → Client message types (sent in broadcasts/responses).", s.ServerToClient) return format.Source([]byte(b.String())) } func renderTS(s schema) string { var b strings.Builder b.WriteString(header("//\n// Shared WebSocket protocol message type constants — single source of truth\n" + "// for both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json\n" + "// and run `make protocol-generate` in Server/.\n//\n" + "// Usage: import { MessageType } from \"@lib/protocolTypes\";\n" + "// ws.send({ type: MessageType.CHAT_SEND, payload: { ... } });")) writeBlock := func(title, name string, msgs []message) { b.WriteString("\n// " + strings.Repeat("-", 75) + "\n") b.WriteString("// " + title + "\n") b.WriteString("// " + strings.Repeat("-", 75) + "\n\n") b.WriteString("export const " + name + " = {\n") for _, m := range msgs { b.WriteString(" " + m.TS + ": " + fmt.Sprintf("%q", m.Wire) + ",") if m.Note != "" { b.WriteString(" // " + m.Note) } b.WriteString("\n") } b.WriteString("} as const;\n\n") b.WriteString("export type " + name + "Value = (typeof " + name + ")[keyof typeof " + name + "];\n") } writeBlock("Server → Client message types", "ServerMessageType", s.ServerToClient) writeBlock("Client → Server message types", "ClientMessageType", s.ClientToServer) b.WriteString("\n// " + strings.Repeat("-", 75) + "\n") b.WriteString("// Unified MessageType — all message types in one object for convenience\n") b.WriteString("// " + strings.Repeat("-", 75) + "\n\n") b.WriteString("export const MessageType = {\n ...ServerMessageType,\n ...ClientMessageType,\n} as const;\n\n") b.WriteString("export type MessageTypeValue = (typeof MessageType)[keyof typeof MessageType];\n") return b.String() }