From d41cef5576419e9c08f06f029dceb3a675929c53 Mon Sep 17 00:00:00 2001 From: edmand46 Date: Mon, 23 Mar 2026 16:04:31 +0300 Subject: [PATCH] feat: added support typescript --- .github/workflows/tests.yml | 97 ++++++ .golangci.yml | 2 +- README.md | 42 ++- cmd/arpack/main.go | 28 +- e2e/e2e_test.go | 299 +++++++++++++++-- generator/ts.go | 635 ++++++++++++++++++++++++++++++++++++ generator/ts_test.go | 401 +++++++++++++++++++++++ 7 files changed, 1469 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 generator/ts.go create mode 100644 generator/ts_test.go diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ead459d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,97 @@ +name: Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.21', '1.22', '1.23'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Download dependencies + run: go mod download + + - name: Run unit tests + run: go test -v ./parser/... ./generator/... + + - name: Run benchmarks (short) + run: go test -bench=. -benchtime=100ms -run=^$ ./benchmarks/... + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + args: --timeout=5m + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Build arpack CLI + run: go build -v ./cmd/arpack + + - name: Test code generation + run: | + go run ./cmd/arpack -in testdata/sample.go -out-go /tmp/gen-go -out-ts /tmp/gen-ts + test -f /tmp/gen-go/Sample_gen.go + test -f /tmp/gen-ts/Sample.gen.ts + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Run E2E tests + run: go test -v ./e2e/... diff --git a/.golangci.yml b/.golangci.yml index f7394a1..17b8dab 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,4 +20,4 @@ formatters: linters-settings: goimports: local-prefixes: - - gorena/server \ No newline at end of file + - github.com/edmand46/arpack \ No newline at end of file diff --git a/README.md b/README.md index a723c01..f8cfe7a 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,19 @@ # ArPack -Binary serialization code generator for Go and C#. Define messages once as Go structs — get zero-allocation `Marshal`/`Unmarshal` for Go and `unsafe` pointer-based `Serialize`/`Deserialize` for C#. +[![Tests](https://github.com/edmand46/arpack/actions/workflows/tests.yml/badge.svg)](https://github.com/edmand46/arpack/actions/workflows/tests.yml) + +Binary serialization code generator for Go, C#, and TypeScript. Define messages once as Go structs — get zero-allocation `Marshal`/`Unmarshal` for Go, `unsafe` pointer-based `Serialize`/`Deserialize` for C#, and `DataView`-based serialization for TypeScript/browser. ## Features -- **Single source of truth** — define messages in Go, generate both Go and C# code +- **Single source of truth** — define messages in Go, generate code for Go, C#, and TypeScript - **Float quantization** — compress `float32`/`float64` to 8 or 16 bits with a `pack` struct tag - **Boolean packing** — consecutive `bool` fields are packed into single bytes (up to 8 per byte) -- **Enums** — `type Opcode uint16` + `const` block becomes a C# `enum` +- **Enums** — `type Opcode uint16` + `const` block becomes C#/TypeScript enums - **Nested types, fixed arrays, slices** — full support for complex message structures -- **Cross-language binary compatibility** — Go and C# produce identical wire formats +- **Cross-language binary compatibility** — Go, C#, and TypeScript produce identical wire formats +- **Browser support** — TypeScript target uses native DataView API for zero-dependency serialization ## When to use @@ -24,6 +27,7 @@ Typical setups: - **[Nakama](https://heroiclabs.com/nakama/) + Unity** — define all network messages in Go, generate C# structs for Unity. Both sides share the exact same wire format with no reflection or boxing. - **Custom Go game server + Unity** — roll your own server without pulling in a serialization framework. ArPack generates plain `Marshal`/`Unmarshal` methods with zero allocations on the hot path. - **Any Go service + .NET client** — works anywhere you control both ends and want a compact binary protocol without Protobuf's runtime overhead or code-gen complexity. +- **Go backend + Browser/WebSocket** — generate TypeScript classes for browser-based clients. Uses native DataView API with zero dependencies. ArPack is a poor fit if you need schema evolution (adding/removing fields without redeploying both sides) — use Protobuf or FlatBuffers instead. @@ -36,7 +40,11 @@ go install github.com/edmand46/arpack/cmd/arpack@latest ## Usage ```bash -arpack -in messages.go -out-go ./gen -out-cs ../Unity/Assets/Scripts +# Generate Go + C# + TypeScript +arpack -in messages.go -out-go ./gen -out-cs ../Unity/Assets/Scripts -out-ts ./web/src/messages + +# Generate only TypeScript +arpack -in messages.go -out-ts ./web/src/messages ``` | Flag | Description | @@ -44,11 +52,13 @@ arpack -in messages.go -out-go ./gen -out-cs ../Unity/Assets/Scripts | `-in` | Input Go file with struct definitions (required) | | `-out-go` | Output directory for generated Go code | | `-out-cs` | Output directory for generated C# code | +| `-out-ts` | Output directory for generated TypeScript code | | `-cs-namespace` | C# namespace (default: `Arpack.Messages`) | **Output files:** - Go: `{name}_gen.go` - C#: `{Name}.gen.cs` +- TypeScript: `{Name}.gen.ts` ## Schema Definition @@ -135,6 +145,28 @@ public static unsafe int Deserialize(byte* buffer, out MoveMessage msg) Uses unsafe pointers for zero-copy serialization. Returns bytes written/consumed. +### TypeScript + +```typescript +export class MoveMessage { + position: Vector3 = new Vector3(); + velocity: number[] = new Array(3).fill(0); + waypoints: Vector3[] = []; + playerId: number = 0; + active: boolean = false; + visible: boolean = false; + ghost: boolean = false; + name: string = ""; + + serialize(view: DataView, offset: number): number + static deserialize(view: DataView, offset: number): [MoveMessage, number] +} +``` + +Uses native DataView API for browser-compatible serialization with zero dependencies. Returns bytes written/consumed. + +**Note:** TypeScript field names are converted to camelCase (e.g., `PlayerID` → `playerId`). + ## Wire Format - Little-endian byte order diff --git a/cmd/arpack/main.go b/cmd/arpack/main.go index 3a36264..a1d99a7 100644 --- a/cmd/arpack/main.go +++ b/cmd/arpack/main.go @@ -1,28 +1,31 @@ package main import ( - "github.com/edmand46/arpack/generator" - "github.com/edmand46/arpack/parser" "flag" "fmt" "log" "os" "path/filepath" "strings" + + "github.com/edmand46/arpack/generator" + "github.com/edmand46/arpack/parser" ) func main() { in := flag.String("in", "", "input Go file with struct definitions") outGo := flag.String("out-go", "", "output directory for generated Go code") outCS := flag.String("out-cs", "", "output directory for generated C# code") + outTS := flag.String("out-ts", "", "output directory for generated TypeScript code") namespace := flag.String("cs-namespace", "Arpack.Messages", "C# namespace") flag.Parse() if *in == "" { log.Fatal("arpack: -in is required") } - if *outGo == "" && *outCS == "" { - log.Fatal("arpack: at least one of -out-go or -out-cs is required") + + if *outGo == "" && *outCS == "" && *outTS == "" { + log.Fatal("arpack: at least one of -out-go, -out-cs, or -out-ts is required") } schema, err := parser.ParseSchemaFile(*in) @@ -74,6 +77,23 @@ func main() { fmt.Printf("arpack: wrote %s\n", outPath) } + + if *outTS != "" { + src, err := generator.GenerateTypeScriptSchema(schema, "Arpack.Messages") + if err != nil { + log.Fatalf("arpack: TypeScript generation error: %v", err) + } + + outPath := filepath.Join(*outTS, toTitle(baseName)+".gen.ts") + if err := os.MkdirAll(*outTS, 0755); err != nil { + log.Fatalf("arpack: mkdir %s: %v", *outTS, err) + } + if err := os.WriteFile(outPath, src, 0644); err != nil { + log.Fatalf("arpack: write %s: %v", outPath, err) + } + + fmt.Printf("arpack: wrote %s\n", outPath) + } } func toTitle(s string) string { diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 8f3320c..5d4030f 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -2,9 +2,9 @@ package e2e import ( "bytes" + "fmt" "github.com/edmand46/arpack/generator" "github.com/edmand46/arpack/parser" - "fmt" "math" "os" "os/exec" @@ -16,12 +16,8 @@ import ( const samplePath = "../testdata/sample.go" -// TestE2E_CrossLanguage гоняет сериализацию в обе стороны: Go → C# и C# → Go. +// TestE2E_CrossLanguage гоняет сериализацию в обе стороны: Go → C# / C# → Go / Go → TS / TS → Go. func TestE2E_CrossLanguage(t *testing.T) { - if _, err := exec.LookPath("dotnet"); err != nil { - t.Skip("dotnet not found, skipping cross-language e2e test") - } - schema, err := parser.ParseSchemaFile(samplePath) if err != nil { t.Fatalf("parse: %v", err) @@ -32,13 +28,7 @@ func TestE2E_CrossLanguage(t *testing.T) { t.Fatalf("GenerateGoSchema: %v", err) } - csSrc, err := generator.GenerateCSharpSchema(schema, "Ragono.Messages") - if err != nil { - t.Fatalf("GenerateCSharpSchema: %v", err) - } - goDir := buildGoHarness(t, goSrc) - csDir := buildCSHarness(t, csSrc) cases := []struct { name string @@ -51,17 +41,52 @@ func TestE2E_CrossLanguage(t *testing.T) { {"EnvelopeMessage", "EnvelopeMessage", 0}, } - for _, tc := range cases { - t.Run("Go_to_CS/"+tc.name, func(t *testing.T) { - hex := runHarness(t, goDir, "go", "ser", tc.typ, "") - out := runHarness(t, csDir, "cs", "deser", tc.typ, hex) - checkOutput(t, tc.typ, out, tc.epsilon) - }) - t.Run("CS_to_Go/"+tc.name, func(t *testing.T) { - hex := runHarness(t, csDir, "cs", "ser", tc.typ, "") - out := runHarness(t, goDir, "go", "deser", tc.typ, hex) - checkOutput(t, tc.typ, out, tc.epsilon) - }) + // C# tests (if dotnet is available) + if _, err := exec.LookPath("dotnet"); err == nil { + csSrc, err := generator.GenerateCSharpSchema(schema, "Ragono.Messages") + if err != nil { + t.Fatalf("GenerateCSharpSchema: %v", err) + } + csDir := buildCSHarness(t, csSrc) + + for _, tc := range cases { + t.Run("Go_to_CS/"+tc.name, func(t *testing.T) { + hex := runHarness(t, goDir, "go", "ser", tc.typ, "") + out := runHarness(t, csDir, "cs", "deser", tc.typ, hex) + checkOutput(t, tc.typ, out, tc.epsilon) + }) + t.Run("CS_to_Go/"+tc.name, func(t *testing.T) { + hex := runHarness(t, csDir, "cs", "ser", tc.typ, "") + out := runHarness(t, goDir, "go", "deser", tc.typ, hex) + checkOutput(t, tc.typ, out, tc.epsilon) + }) + } + } else { + t.Log("dotnet not found, skipping C# cross-language e2e tests") + } + + // TypeScript tests (if node and tsx are available) + if _, err := exec.LookPath("node"); err == nil { + tsSrc, err := generator.GenerateTypeScriptSchema(schema, "Arpack.Messages") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + tsDir := buildTSHarness(t, tsSrc) + + for _, tc := range cases { + t.Run("Go_to_TS/"+tc.name, func(t *testing.T) { + hex := runHarness(t, goDir, "go", "ser", tc.typ, "") + out := runHarness(t, tsDir, "ts", "deser", tc.typ, hex) + checkOutput(t, tc.typ, out, tc.epsilon) + }) + t.Run("TS_to_Go/"+tc.name, func(t *testing.T) { + hex := runHarness(t, tsDir, "ts", "ser", tc.typ, "") + out := runHarness(t, goDir, "go", "deser", tc.typ, hex) + checkOutput(t, tc.typ, out, tc.epsilon) + }) + } + } else { + t.Log("node not found, skipping TypeScript cross-language e2e tests") } } @@ -100,23 +125,59 @@ func buildCSHarness(t *testing.T, generatedSrc []byte) string { return dir } +func buildTSHarness(t *testing.T, generatedSrc []byte) string { + t.Helper() + dir := t.TempDir() + + // Create src directory + srcDir := filepath.Join(dir, "src") + if err := os.MkdirAll(srcDir, 0755); err != nil { + t.Fatalf("mkdir %s: %v", srcDir, err) + } + + // Write generated messages + write(t, filepath.Join(srcDir, "messages.gen.ts"), generatedSrc) + + // Write harness + write(t, filepath.Join(srcDir, "harness.ts"), []byte(tsHarnessSource)) + + // Write package.json + write(t, filepath.Join(dir, "package.json"), []byte(tsPackageSource)) + + // Write tsconfig.json + write(t, filepath.Join(dir, "tsconfig.json"), []byte(tsConfigSource)) + + // Install dependencies and build + mustRun(t, dir, "npm", "install") + mustRun(t, dir, "npx", "tsc") + + return dir +} + // --- Harness runners --- func runHarness(t *testing.T, dir, lang, op, typ, hexInput string) string { t.Helper() var cmd *exec.Cmd - if lang == "go" { + switch lang { + case "go": args := []string{op, typ} if hexInput != "" { args = append(args, hexInput) } cmd = exec.Command(filepath.Join(dir, "harness"), args...) - } else { + case "cs": args := []string{op, typ} if hexInput != "" { args = append(args, hexInput) } cmd = exec.Command("dotnet", append([]string{filepath.Join(dir, "out", "E2EHarness.dll")}, args...)...) + case "ts": + args := []string{op, typ} + if hexInput != "" { + args = append(args, hexInput) + } + cmd = exec.Command("node", append([]string{filepath.Join(dir, "dist", "harness.js")}, args...)...) } cmd.Dir = dir out, err := cmd.CombinedOutput() @@ -519,3 +580,191 @@ var csProjSource = fmt.Sprintf(` `) + +// --- TypeScript harness source --- + +const tsPackageSource = `{ + "name": "arpack-e2e-harness", + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "tsc" + }, + "devDependencies": { + "typescript": "^5.3.0", + "@types/node": "^20.0.0" + } +} +` + +const tsConfigSource = `{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +` + +const tsHarnessSource = `import { readFileSync } from 'fs'; +import { argv } from 'process'; + +// Import generated messages +import { Vector3, MoveMessage, SpawnMessage, EnvelopeMessage, Opcode } from './messages.gen.js'; + +// Hex encoding/decoding utilities +function encodeHex(data: Uint8Array): string { + return Array.from(data) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + +function decodeHex(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes; +} + +// Main harness +function main() { + const args = argv.slice(2); + const op = args[0]; // 'ser' or 'deser' + const typ = args[1]; // message type + const hexInput = args[2]; // for deser + + switch (` + "`${op}:${typ}`" + `) { + case 'ser:Vector3': { + const msg = new Vector3(); + msg.x = 123.45; + msg.y = -200.0; + msg.z = 0.0; + const buf = new ArrayBuffer(64); + const view = new DataView(buf); + const n = msg.serialize(view, 0); + const bytes = new Uint8Array(buf, 0, n); + console.log(encodeHex(bytes)); + break; + } + + case 'deser:Vector3': { + const data = decodeHex(hexInput); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const [msg] = Vector3.deserialize(view, 0); + console.log(` + "`X=${msg.x.toPrecision(9)}`" + `); + console.log(` + "`Y=${msg.y.toPrecision(9)}`" + `); + console.log(` + "`Z=${msg.z.toPrecision(9)}`" + `); + break; + } + + case 'ser:SpawnMessage': { + const msg = new SpawnMessage(); + msg.entityID = 42n; + msg.position = new Vector3(); + msg.position.x = 10.0; + msg.position.y = 20.0; + msg.position.z = 30.0; + msg.health = -100; + msg.tags = ['hero', 'player']; + msg.data = [1, 2, 3]; + const buf = new ArrayBuffer(512); + const view = new DataView(buf); + const n = msg.serialize(view, 0); + const bytes = new Uint8Array(buf, 0, n); + console.log(encodeHex(bytes)); + break; + } + + case 'deser:SpawnMessage': { + const data = decodeHex(hexInput); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const [msg] = SpawnMessage.deserialize(view, 0); + console.log(` + "`EntityID=${msg.entityID.toString()}`" + `); + console.log(` + "`Position.X=${msg.position.x.toPrecision(9)}`" + `); + console.log(` + "`Position.Y=${msg.position.y.toPrecision(9)}`" + `); + console.log(` + "`Position.Z=${msg.position.z.toPrecision(9)}`" + `); + console.log(` + "`Health=${msg.health}`" + `); + for (let i = 0; i < msg.tags.length; i++) { + console.log(` + "`Tags[${i}]=${msg.tags[i]}`" + `); + } + for (let i = 0; i < msg.data.length; i++) { + console.log(` + "`Data[${i}]=${msg.data[i]}`" + `); + } + break; + } + + case 'ser:MoveMessage': { + const msg = new MoveMessage(); + msg.position = new Vector3(); + msg.position.x = 50.0; + msg.position.y = -100.0; + msg.position.z = 0.0; + msg.velocity = [1.5, -2.5, 0.0]; + msg.waypoints = [new Vector3()]; + msg.waypoints[0].x = 10.0; + msg.waypoints[0].y = 20.0; + msg.waypoints[0].z = 0.0; + msg.playerID = 777; + msg.active = true; + msg.visible = false; + msg.ghost = true; + msg.name = 'TestPlayer'; + const buf = new ArrayBuffer(512); + const view = new DataView(buf); + const n = msg.serialize(view, 0); + const bytes = new Uint8Array(buf, 0, n); + console.log(encodeHex(bytes)); + break; + } + + case 'deser:MoveMessage': { + const data = decodeHex(hexInput); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const [msg] = MoveMessage.deserialize(view, 0); + console.log(` + "`PlayerID=${msg.playerID}`" + `); + console.log(` + "`Active=${msg.active.toString().toLowerCase()}`" + `); + console.log(` + "`Visible=${msg.visible.toString().toLowerCase()}`" + `); + console.log(` + "`Ghost=${msg.ghost.toString().toLowerCase()}`" + `); + console.log(` + "`Name=${msg.name}`" + `); + break; + } + + case 'ser:EnvelopeMessage': { + const msg = new EnvelopeMessage(); + msg.code = 2; // Opcode.JoinRoom + msg.counter = 7; + const buf = new ArrayBuffer(64); + const view = new DataView(buf); + const n = msg.serialize(view, 0); + const bytes = new Uint8Array(buf, 0, n); + console.log(encodeHex(bytes)); + break; + } + + case 'deser:EnvelopeMessage': { + const data = decodeHex(hexInput); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const [msg] = EnvelopeMessage.deserialize(view, 0); + console.log(` + "`Code=${msg.code}`" + `); + console.log(` + "`Counter=${msg.counter}`" + `); + break; + } + + default: + console.error(` + "`Unknown op:type ${op}:${typ}`" + `); + process.exit(1); + } +} + +main(); +` diff --git a/generator/ts.go b/generator/ts.go new file mode 100644 index 0000000..e261e61 --- /dev/null +++ b/generator/ts.go @@ -0,0 +1,635 @@ +package generator + +import ( + "fmt" + "strings" + + "github.com/edmand46/arpack/parser" +) + +// GenerateTypeScript generates TypeScript code for the given messages +func GenerateTypeScript(messages []parser.Message, namespace string) ([]byte, error) { + return GenerateTypeScriptSchema(parser.Schema{Messages: messages}, namespace) +} + +// GenerateTypeScriptSchema generates TypeScript code from a schema +func GenerateTypeScriptSchema(schema parser.Schema, namespace string) ([]byte, error) { + messages := schema.Messages + var b strings.Builder + + b.WriteString("// arpack \n") + b.WriteString("// Code generated by arpack. DO NOT EDIT.\n\n") + + enumNames := make(map[string]struct{}, len(schema.Enums)) + for _, enum := range schema.Enums { + enumNames[enum.Name] = struct{}{} + } + + // Generate enums + for _, enum := range schema.Enums { + writeTSEnum(&b, enum) + b.WriteString("\n") + } + + // Generate messages + for i, msg := range messages { + if err := writeTSMessage(&b, msg, enumNames); err != nil { + return nil, fmt.Errorf("message %s: %w", msg.Name, err) + } + if i < len(messages)-1 { + b.WriteString("\n") + } + } + + return []byte(b.String()), nil +} + +func writeTSEnum(b *strings.Builder, enum parser.Enum) { + fmt.Fprintf(b, "export enum %s {\n", enum.Name) + for i, value := range enum.Values { + fmt.Fprintf(b, " %s = %s", value.Name, value.Value) + if i < len(enum.Values)-1 { + b.WriteString(",") + } + b.WriteString("\n") + } + b.WriteString("}\n") +} + +func writeTSMessage(b *strings.Builder, msg parser.Message, enumNames map[string]struct{}) error { + segs := segmentFields(msg.Fields) + + fmt.Fprintf(b, "export class %s {\n", msg.Name) + + // Field declarations with defaults + for _, f := range msg.Fields { + defaultValue := tsDefaultValue(f) + fmt.Fprintf(b, " %s: %s = %s;\n", toCamelCase(f.Name), tsTypeName(f, enumNames), defaultValue) + } + b.WriteString("\n") + + // Serialize method + b.WriteString(" serialize(view: DataView, offset: number): number {\n") + b.WriteString(" let pos = offset;\n") + for i, seg := range segs { + if seg.single != nil { + if err := writeTSSerializeField(b, "this", *seg.single, " ", enumNames); err != nil { + return err + } + } else { + writeTSBoolGroupSerialize(b, "this", seg.bools, i, " ") + } + } + b.WriteString(" return pos - offset;\n") + b.WriteString(" }\n\n") + + // Deserialize method + fmt.Fprintf(b, " static deserialize(view: DataView, offset: number): [%s, number] {\n", msg.Name) + b.WriteString(" let pos = offset;\n") + b.WriteString(fmt.Sprintf(" const msg = new %s();\n", msg.Name)) + for i, seg := range segs { + if seg.single != nil { + if err := writeTSDeserializeField(b, "msg", *seg.single, " ", enumNames); err != nil { + return err + } + } else { + writeTSBoolGroupDeserialize(b, "msg", seg.bools, i, " ") + } + } + b.WriteString(" return [msg, pos - offset];\n") + b.WriteString(" }\n") + + b.WriteString("}\n") + return nil +} + +func writeTSBoolGroupSerialize(b *strings.Builder, recv string, bools []parser.Field, groupIdx int, indent string) { + varName := fmt.Sprintf("_boolByte%d", groupIdx) + fmt.Fprintf(b, "%slet %s = 0;\n", indent, varName) + for bit, f := range bools { + fmt.Fprintf(b, "%sif (%s.%s) %s |= 1 << %d;\n", indent, recv, toCamelCase(f.Name), varName, bit) + } + fmt.Fprintf(b, "%sview.setUint8(pos, %s);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) +} + +func writeTSBoolGroupDeserialize(b *strings.Builder, recv string, bools []parser.Field, groupIdx int, indent string) { + varName := fmt.Sprintf("_boolByte%d", groupIdx) + fmt.Fprintf(b, "%sconst %s = view.getUint8(pos);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + for bit, f := range bools { + fmt.Fprintf(b, "%s%s.%s = (%s & (1 << %d)) !== 0;\n", indent, recv, toCamelCase(f.Name), varName, bit) + } +} + +func writeTSSerializeField(b *strings.Builder, recv string, f parser.Field, indent string, enumNames map[string]struct{}) error { + access := recv + "." + toCamelCase(f.Name) + switch f.Kind { + case parser.KindPrimitive: + return writeTSSerializePrimitive(b, access, f, indent, enumNames) + case parser.KindNested: + fmt.Fprintf(b, "%spos += %s.serialize(view, pos);\n", indent, access) + case parser.KindFixedArray: + iVar := "_i" + f.Name + fmt.Fprintf(b, "%sfor (let %s = 0; %s < %d; %s++) {\n", + indent, iVar, iVar, f.FixedLen, iVar) + elemField := parser.Field{ + Name: f.Name + "[" + iVar + "]", + Kind: f.Elem.Kind, + Primitive: f.Elem.Primitive, + NamedType: f.Elem.NamedType, + Quant: f.Elem.Quant, + TypeName: f.Elem.TypeName, + Elem: f.Elem.Elem, + FixedLen: f.Elem.FixedLen, + } + if err := writeTSSerializeField(b, recv, elemField, indent+" ", enumNames); err != nil { + return err + } + fmt.Fprintf(b, "%s}\n", indent) + case parser.KindSlice: + fmt.Fprintf(b, "%sview.setUint16(pos, %s.length, true);\n", indent, access) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + iVar := "_i" + f.Name + fmt.Fprintf(b, "%sfor (const %s of %s) {\n", indent, iVar, access) + elemField := parser.Field{ + Name: iVar, + Kind: f.Elem.Kind, + Primitive: f.Elem.Primitive, + NamedType: f.Elem.NamedType, + Quant: f.Elem.Quant, + TypeName: f.Elem.TypeName, + Elem: f.Elem.Elem, + FixedLen: f.Elem.FixedLen, + } + if err := writeTSSerializeFieldElement(b, iVar, elemField, indent+" ", enumNames); err != nil { + return err + } + fmt.Fprintf(b, "%s}\n", indent) + } + return nil +} + +func writeTSSerializeFieldElement(b *strings.Builder, access string, f parser.Field, indent string, enumNames map[string]struct{}) error { + switch f.Kind { + case parser.KindPrimitive: + return writeTSSerializePrimitiveElement(b, access, f, indent, enumNames) + case parser.KindNested: + fmt.Fprintf(b, "%spos += %s.serialize(view, pos);\n", indent, access) + } + return nil +} + +func writeTSSerializePrimitiveElement(b *strings.Builder, access string, f parser.Field, indent string, enumNames map[string]struct{}) error { + if f.Quant != nil { + return writeTSSerializeQuantElement(b, access, f, indent) + } + + valueExpr := tsSerializeValueExpr(access, f, enumNames) + switch f.Primitive { + case parser.KindFloat32: + fmt.Fprintf(b, "%sview.setFloat32(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindFloat64: + fmt.Fprintf(b, "%sview.setFloat64(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindInt8: + fmt.Fprintf(b, "%sview.setInt8(pos, %s);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindUint8: + fmt.Fprintf(b, "%sview.setUint8(pos, %s);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindBool: + fmt.Fprintf(b, "%sview.setUint8(pos, %s ? 1 : 0);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindInt16: + fmt.Fprintf(b, "%sview.setInt16(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindUint16: + fmt.Fprintf(b, "%sview.setUint16(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindInt32: + fmt.Fprintf(b, "%sview.setInt32(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindUint32: + fmt.Fprintf(b, "%sview.setUint32(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindInt64: + fmt.Fprintf(b, "%sview.setBigInt64(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindUint64: + fmt.Fprintf(b, "%sview.setBigUint64(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindString: + lenVar := "_slen" + sanitizeVarName(access) + fmt.Fprintf(b, "%sconst %s = new TextEncoder().encode(%s);\n", indent, lenVar, valueExpr) + fmt.Fprintf(b, "%sview.setUint16(pos, %s.length, true);\n", indent, lenVar) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + fmt.Fprintf(b, "%snew Uint8Array(view.buffer, pos, %s.length).set(%s);\n", indent, lenVar, lenVar) + fmt.Fprintf(b, "%spos += %s.length;\n", indent, lenVar) + } + return nil +} + +func writeTSSerializePrimitive(b *strings.Builder, access string, f parser.Field, indent string, enumNames map[string]struct{}) error { + if f.Quant != nil { + return writeTSSerializeQuant(b, access, f, indent) + } + + valueExpr := tsSerializeValueExpr(access, f, enumNames) + switch f.Primitive { + case parser.KindFloat32: + fmt.Fprintf(b, "%sview.setFloat32(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindFloat64: + fmt.Fprintf(b, "%sview.setFloat64(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindInt8: + fmt.Fprintf(b, "%sview.setInt8(pos, %s);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindUint8: + fmt.Fprintf(b, "%sview.setUint8(pos, %s);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindBool: + fmt.Fprintf(b, "%sview.setUint8(pos, %s ? 1 : 0);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindInt16: + fmt.Fprintf(b, "%sview.setInt16(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindUint16: + fmt.Fprintf(b, "%sview.setUint16(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindInt32: + fmt.Fprintf(b, "%sview.setInt32(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindUint32: + fmt.Fprintf(b, "%sview.setUint32(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindInt64: + fmt.Fprintf(b, "%sview.setBigInt64(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindUint64: + fmt.Fprintf(b, "%sview.setBigUint64(pos, %s, true);\n", indent, valueExpr) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindString: + lenVar := "_slen" + sanitizeVarName(access) + fmt.Fprintf(b, "%sconst %s = new TextEncoder().encode(%s);\n", indent, lenVar, valueExpr) + fmt.Fprintf(b, "%sview.setUint16(pos, %s.length, true);\n", indent, lenVar) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + fmt.Fprintf(b, "%snew Uint8Array(view.buffer, pos, %s.length).set(%s);\n", indent, lenVar, lenVar) + fmt.Fprintf(b, "%spos += %s.length;\n", indent, lenVar) + } + return nil +} + +func writeTSSerializeQuant(b *strings.Builder, access string, f parser.Field, indent string) error { + q := f.Quant + maxUint := q.MaxUint() + varName := "_q" + sanitizeVarName(access) + if q.Bits == 8 { + fmt.Fprintf(b, "%sconst %s = Math.round((%s - (%g)) / (%g - (%g)) * %g);\n", + indent, varName, access, q.Min, q.Max, q.Min, maxUint) + fmt.Fprintf(b, "%sview.setUint8(pos, %s);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + } else { + fmt.Fprintf(b, "%sconst %s = Math.round((%s - (%g)) / (%g - (%g)) * %g);\n", + indent, varName, access, q.Min, q.Max, q.Min, maxUint) + fmt.Fprintf(b, "%sview.setUint16(pos, %s, true);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + } + return nil +} + +func writeTSSerializeQuantElement(b *strings.Builder, access string, f parser.Field, indent string) error { + q := f.Quant + maxUint := q.MaxUint() + varName := "_q" + sanitizeVarName(access) + if q.Bits == 8 { + fmt.Fprintf(b, "%sconst %s = Math.round((%s - (%g)) / (%g - (%g)) * %g);\n", + indent, varName, access, q.Min, q.Max, q.Min, maxUint) + fmt.Fprintf(b, "%sview.setUint8(pos, %s);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + } else { + fmt.Fprintf(b, "%sconst %s = Math.round((%s - (%g)) / (%g - (%g)) * %g);\n", + indent, varName, access, q.Min, q.Max, q.Min, maxUint) + fmt.Fprintf(b, "%sview.setUint16(pos, %s, true);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + } + return nil +} + +func writeTSDeserializeField(b *strings.Builder, recv string, f parser.Field, indent string, enumNames map[string]struct{}) error { + access := recv + "." + toCamelCase(f.Name) + switch f.Kind { + case parser.KindPrimitive: + return writeTSDeserializePrimitive(b, access, f, indent, enumNames) + case parser.KindNested: + fmt.Fprintf(b, "%sconst [_%s, _n%s] = %s.deserialize(view, pos);\n", indent, f.Name, f.Name, f.TypeName) + fmt.Fprintf(b, "%s%s = _%s;\n", indent, access, f.Name) + fmt.Fprintf(b, "%spos += _n%s;\n", indent, f.Name) + case parser.KindFixedArray: + iVar := "_i" + f.Name + fmt.Fprintf(b, "%s%s = new Array(%d);\n", indent, access, f.FixedLen) + fmt.Fprintf(b, "%sfor (let %s = 0; %s < %d; %s++) {\n", + indent, iVar, iVar, f.FixedLen, iVar) + elemField := parser.Field{ + Name: f.Name + "[" + iVar + "]", + Kind: f.Elem.Kind, + Primitive: f.Elem.Primitive, + NamedType: f.Elem.NamedType, + Quant: f.Elem.Quant, + TypeName: f.Elem.TypeName, + Elem: f.Elem.Elem, + FixedLen: f.Elem.FixedLen, + } + if err := writeTSDeserializeField(b, recv, elemField, indent+" ", enumNames); err != nil { + return err + } + fmt.Fprintf(b, "%s}\n", indent) + case parser.KindSlice: + lenVar := "_len" + f.Name + fmt.Fprintf(b, "%sconst %s = view.getUint16(pos, true);\n", indent, lenVar) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + fmt.Fprintf(b, "%s%s = new Array(%s);\n", indent, access, lenVar) + iVar := "_i" + f.Name + fmt.Fprintf(b, "%sfor (let %s = 0; %s < %s; %s++) {\n", indent, iVar, iVar, lenVar, iVar) + elemField := parser.Field{ + Name: f.Name + "[" + iVar + "]", + Kind: f.Elem.Kind, + Primitive: f.Elem.Primitive, + NamedType: f.Elem.NamedType, + Quant: f.Elem.Quant, + TypeName: f.Elem.TypeName, + Elem: f.Elem.Elem, + FixedLen: f.Elem.FixedLen, + } + if err := writeTSDeserializeFieldElement(b, recv, elemField, indent+" ", enumNames); err != nil { + return err + } + fmt.Fprintf(b, "%s}\n", indent) + } + return nil +} + +func writeTSDeserializeFieldElement(b *strings.Builder, recv string, f parser.Field, indent string, enumNames map[string]struct{}) error { + access := recv + "." + toCamelCase(f.Name) + switch f.Kind { + case parser.KindPrimitive: + return writeTSDeserializePrimitiveElement(b, access, f, indent, enumNames) + case parser.KindNested: + fmt.Fprintf(b, "%sconst [_elem, _nElem] = %s.deserialize(view, pos);\n", indent, f.TypeName) + fmt.Fprintf(b, "%s%s = _elem;\n", indent, access) + fmt.Fprintf(b, "%spos += _nElem;\n", indent) + } + return nil +} + +func writeTSDeserializePrimitiveElement(b *strings.Builder, access string, f parser.Field, indent string, enumNames map[string]struct{}) error { + if f.Quant != nil { + return writeTSDeserializeQuantElement(b, access, f, indent) + } + + switch f.Primitive { + case parser.KindFloat32: + expr := fmt.Sprintf("view.getFloat32(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindFloat64: + expr := fmt.Sprintf("view.getFloat64(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindInt8: + expr := fmt.Sprintf("view.getInt8(pos)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindUint8: + expr := fmt.Sprintf("view.getUint8(pos)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindBool: + expr := fmt.Sprintf("view.getUint8(pos) !== 0") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindInt16: + expr := fmt.Sprintf("view.getInt16(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindUint16: + expr := fmt.Sprintf("view.getUint16(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindInt32: + expr := fmt.Sprintf("view.getInt32(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindUint32: + expr := fmt.Sprintf("view.getUint32(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindInt64: + expr := fmt.Sprintf("view.getBigInt64(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindUint64: + expr := fmt.Sprintf("view.getBigUint64(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindString: + lenVar := "_slen" + sanitizeVarName(access) + fmt.Fprintf(b, "%sconst %s = view.getUint16(pos, true);\n", indent, lenVar) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + expr := fmt.Sprintf("new TextDecoder().decode(new Uint8Array(view.buffer, pos, %s))", lenVar) + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + fmt.Fprintf(b, "%spos += %s;\n", indent, lenVar) + } + return nil +} + +func writeTSDeserializePrimitive(b *strings.Builder, access string, f parser.Field, indent string, enumNames map[string]struct{}) error { + if f.Quant != nil { + return writeTSDeserializeQuant(b, access, f, indent) + } + + switch f.Primitive { + case parser.KindFloat32: + expr := fmt.Sprintf("view.getFloat32(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindFloat64: + expr := fmt.Sprintf("view.getFloat64(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindInt8: + expr := fmt.Sprintf("view.getInt8(pos)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindUint8: + expr := fmt.Sprintf("view.getUint8(pos)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindBool: + expr := fmt.Sprintf("view.getUint8(pos) !== 0") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + case parser.KindInt16: + expr := fmt.Sprintf("view.getInt16(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindUint16: + expr := fmt.Sprintf("view.getUint16(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + case parser.KindInt32: + expr := fmt.Sprintf("view.getInt32(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindUint32: + expr := fmt.Sprintf("view.getUint32(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 4;\n", indent)) + case parser.KindInt64: + expr := fmt.Sprintf("view.getBigInt64(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindUint64: + expr := fmt.Sprintf("view.getBigUint64(pos, true)") + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + b.WriteString(fmt.Sprintf("%spos += 8;\n", indent)) + case parser.KindString: + lenVar := "_slen" + sanitizeVarName(access) + fmt.Fprintf(b, "%sconst %s = view.getUint16(pos, true);\n", indent, lenVar) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + expr := fmt.Sprintf("new TextDecoder().decode(new Uint8Array(view.buffer, pos, %s))", lenVar) + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, enumNames)) + fmt.Fprintf(b, "%spos += %s;\n", indent, lenVar) + } + return nil +} + +func writeTSDeserializeQuant(b *strings.Builder, access string, f parser.Field, indent string) error { + q := f.Quant + maxUint := q.MaxUint() + varName := "_q" + sanitizeVarName(access) + if q.Bits == 8 { + fmt.Fprintf(b, "%sconst %s = view.getUint8(pos);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + expr := fmt.Sprintf("%s / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min) + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, nil)) + } else { + fmt.Fprintf(b, "%sconst %s = view.getUint16(pos, true);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + expr := fmt.Sprintf("%s / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min) + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, nil)) + } + return nil +} + +func writeTSDeserializeQuantElement(b *strings.Builder, access string, f parser.Field, indent string) error { + q := f.Quant + maxUint := q.MaxUint() + varName := "_q" + sanitizeVarName(access) + if q.Bits == 8 { + fmt.Fprintf(b, "%sconst %s = view.getUint8(pos);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 1;\n", indent)) + expr := fmt.Sprintf("%s / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min) + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, nil)) + } else { + fmt.Fprintf(b, "%sconst %s = view.getUint16(pos, true);\n", indent, varName) + b.WriteString(fmt.Sprintf("%spos += 2;\n", indent)) + expr := fmt.Sprintf("%s / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min) + fmt.Fprintf(b, "%s%s = %s;\n", indent, access, tsDeserializeValueExpr(expr, f, nil)) + } + return nil +} + +func tsTypeName(f parser.Field, enumNames map[string]struct{}) string { + switch f.Kind { + case parser.KindPrimitive: + if tsIsEnumType(f, enumNames) { + return f.NamedType + } + return tsPrimitiveTypeName(f.Primitive) + case parser.KindNested: + return f.TypeName + case parser.KindFixedArray, parser.KindSlice: + return tsTypeName(*f.Elem, enumNames) + "[]" + } + return "unknown" +} + +func tsPrimitiveTypeName(k parser.PrimitiveKind) string { + switch k { + case parser.KindFloat32, parser.KindFloat64: + return "number" + case parser.KindInt8, parser.KindInt16, parser.KindInt32, parser.KindUint8, parser.KindUint16, parser.KindUint32: + return "number" + case parser.KindInt64, parser.KindUint64: + return "bigint" + case parser.KindBool: + return "boolean" + case parser.KindString: + return "string" + } + return "unknown" +} + +func tsDefaultValue(f parser.Field) string { + switch f.Kind { + case parser.KindPrimitive: + if tsIsEnumType(f, nil) { + return "0" + } + switch f.Primitive { + case parser.KindFloat32, parser.KindFloat64, parser.KindInt8, parser.KindInt16, parser.KindInt32, + parser.KindUint8, parser.KindUint16, parser.KindUint32: + return "0" + case parser.KindInt64, parser.KindUint64: + return "0n" + case parser.KindBool: + return "false" + case parser.KindString: + return `""` + } + case parser.KindNested: + return fmt.Sprintf("new %s()", f.TypeName) + case parser.KindFixedArray: + elemDefault := tsDefaultValue(*f.Elem) + elemType := tsTypeName(*f.Elem, nil) + return fmt.Sprintf("new Array<%s>(%d).fill(%s)", elemType, f.FixedLen, elemDefault) + case parser.KindSlice: + return "[]" + } + return "undefined" +} + +func tsSerializeValueExpr(access string, f parser.Field, enumNames map[string]struct{}) string { + if !tsIsEnumType(f, enumNames) { + return access + } + return access + " as " + tsPrimitiveTypeName(f.Primitive) +} + +func tsDeserializeValueExpr(expr string, f parser.Field, enumNames map[string]struct{}) string { + if !tsIsEnumType(f, enumNames) { + return expr + } + return "(" + expr + " as " + f.NamedType + ")" +} + +func tsIsEnumType(f parser.Field, enumNames map[string]struct{}) bool { + if f.NamedType == "" || enumNames == nil { + return false + } + _, ok := enumNames[f.NamedType] + return ok +} + +// toCamelCase converts PascalCase to camelCase (e.g., EntityID -> entityID) +func toCamelCase(s string) string { + if s == "" { + return "" + } + // First character to lowercase + result := strings.ToLower(s[:1]) + s[1:] + return result +} diff --git a/generator/ts_test.go b/generator/ts_test.go new file mode 100644 index 0000000..1c578c7 --- /dev/null +++ b/generator/ts_test.go @@ -0,0 +1,401 @@ +package generator + +import ( + "strings" + "testing" + + "github.com/edmand46/arpack/parser" +) + +func TestGenerateTypeScript_Primitives(t *testing.T) { + schema := parser.Schema{ + Messages: []parser.Message{ + { + PackageName: "test", + Name: "PrimitiveMessage", + Fields: []parser.Field{ + {Name: "F32", Kind: parser.KindPrimitive, Primitive: parser.KindFloat32}, + {Name: "F64", Kind: parser.KindPrimitive, Primitive: parser.KindFloat64}, + {Name: "I8", Kind: parser.KindPrimitive, Primitive: parser.KindInt8}, + {Name: "I16", Kind: parser.KindPrimitive, Primitive: parser.KindInt16}, + {Name: "I32", Kind: parser.KindPrimitive, Primitive: parser.KindInt32}, + {Name: "I64", Kind: parser.KindPrimitive, Primitive: parser.KindInt64}, + {Name: "U8", Kind: parser.KindPrimitive, Primitive: parser.KindUint8}, + {Name: "U16", Kind: parser.KindPrimitive, Primitive: parser.KindUint16}, + {Name: "U32", Kind: parser.KindPrimitive, Primitive: parser.KindUint32}, + {Name: "U64", Kind: parser.KindPrimitive, Primitive: parser.KindUint64}, + {Name: "B", Kind: parser.KindPrimitive, Primitive: parser.KindBool}, + {Name: "S", Kind: parser.KindPrimitive, Primitive: parser.KindString}, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check field declarations (now using camelCase) + if !strings.Contains(code, "f32: number = 0;") { + t.Error("Missing f32 field") + } + if !strings.Contains(code, "i64: bigint = 0n;") { + t.Error("Missing i64 field with bigint type") + } + if !strings.Contains(code, "u64: bigint = 0n;") { + t.Error("Missing u64 field with bigint type") + } + if !strings.Contains(code, "b: boolean = false;") { + t.Error("Missing b field") + } + if !strings.Contains(code, "s: string = \"\";") { + t.Error("Missing s field") + } + + // Check serialize method exists + if !strings.Contains(code, "serialize(view: DataView, offset: number): number") { + t.Error("Missing serialize method") + } + + // Check deserialize method exists + if !strings.Contains(code, "static deserialize(view: DataView, offset: number): [PrimitiveMessage, number]") { + t.Error("Missing deserialize method") + } +} + +func TestGenerateTypeScript_QuantizedFloats(t *testing.T) { + schema := parser.Schema{ + Messages: []parser.Message{ + { + PackageName: "test", + Name: "QuantMessage", + Fields: []parser.Field{ + { + Name: "Q8", + Kind: parser.KindPrimitive, + Primitive: parser.KindFloat32, + Quant: &parser.QuantInfo{Min: 0, Max: 100, Bits: 8}, + }, + { + Name: "Q16", + Kind: parser.KindPrimitive, + Primitive: parser.KindFloat32, + Quant: &parser.QuantInfo{Min: -500, Max: 500, Bits: 16}, + }, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check 8-bit quantization (using camelCase field names) + if !strings.Contains(code, "Math.round((this.q8 - (0)) / (100 - (0)) * 255)") { + t.Error("Missing 8-bit quantization code") + } + + // Check 16-bit quantization (using camelCase field names) + if !strings.Contains(code, "Math.round((this.q16 - (-500)) / (500 - (-500)) * 65535)") { + t.Error("Missing 16-bit quantization code") + } + + // Check deserialization with dequantization + if !strings.Contains(code, "/ 255 * (100 - (0)) + (0)") { + t.Error("Missing 8-bit dequantization") + } + if !strings.Contains(code, "/ 65535 * (500 - (-500)) + (-500)") { + t.Error("Missing 16-bit dequantization") + } +} + +func TestGenerateTypeScript_BoolPacking(t *testing.T) { + schema := parser.Schema{ + Messages: []parser.Message{ + { + PackageName: "test", + Name: "BoolMessage", + Fields: []parser.Field{ + {Name: "A", Kind: parser.KindPrimitive, Primitive: parser.KindBool}, + {Name: "B", Kind: parser.KindPrimitive, Primitive: parser.KindBool}, + {Name: "C", Kind: parser.KindPrimitive, Primitive: parser.KindBool}, + {Name: "X", Kind: parser.KindPrimitive, Primitive: parser.KindUint32}, + {Name: "D", Kind: parser.KindPrimitive, Primitive: parser.KindBool}, + {Name: "E", Kind: parser.KindPrimitive, Primitive: parser.KindBool}, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check that consecutive bools are packed (using camelCase field names) + if !strings.Contains(code, "let _boolByte0 = 0;") { + t.Error("Missing first bool group packing") + } + if !strings.Contains(code, "if (this.a) _boolByte0 |= 1 << 0;") { + t.Error("Missing a bool packing") + } + if !strings.Contains(code, "if (this.b) _boolByte0 |= 1 << 1;") { + t.Error("Missing b bool packing") + } + if !strings.Contains(code, "if (this.c) _boolByte0 |= 1 << 2;") { + t.Error("Missing c bool packing") + } + + // Check second bool group after uint32 (index is 2, not 4, based on segment index) + if !strings.Contains(code, "let _boolByte2 = 0;") { + t.Error("Missing second bool group packing") + } + + // Check deserialization (using camelCase field names) + if !strings.Contains(code, "msg.a = (_boolByte0 & (1 << 0)) !== 0;") { + t.Error("Missing a bool unpacking") + } +} + +func TestGenerateTypeScript_NestedTypes(t *testing.T) { + schema := parser.Schema{ + Messages: []parser.Message{ + { + PackageName: "test", + Name: "Inner", + Fields: []parser.Field{ + {Name: "Value", Kind: parser.KindPrimitive, Primitive: parser.KindInt32}, + }, + }, + { + PackageName: "test", + Name: "Outer", + Fields: []parser.Field{ + {Name: "Inner", Kind: parser.KindNested, TypeName: "Inner"}, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check nested type default value (using camelCase field name) + if !strings.Contains(code, "inner: Inner = new Inner();") { + t.Error("Missing nested type field with default") + } + + // Check serialize calls nested serialize (using camelCase field name) + if !strings.Contains(code, "pos += this.inner.serialize(view, pos);") { + t.Error("Missing nested serialize call") + } + + // Check deserialize calls nested deserialize + if !strings.Contains(code, "const [_Inner, _nInner] = Inner.deserialize(view, pos);") { + t.Error("Missing nested deserialize call") + } +} + +func TestGenerateTypeScript_FixedArrays(t *testing.T) { + schema := parser.Schema{ + Messages: []parser.Message{ + { + PackageName: "test", + Name: "ArrayMessage", + Fields: []parser.Field{ + { + Name: "Values", + Kind: parser.KindFixedArray, + FixedLen: 3, + Elem: &parser.Field{ + Kind: parser.KindPrimitive, + Primitive: parser.KindFloat32, + }, + }, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check default value (using camelCase field name) + if !strings.Contains(code, "values: number[] = new Array(3).fill(0);") { + t.Error("Missing fixed array field with default") + } + + // Check serialization loop (using camelCase field name) + if !strings.Contains(code, "for (let _iValues = 0; _iValues < 3; _iValues++)") { + t.Error("Missing fixed array serialization loop") + } + + // Check deserialization loop (using camelCase field name) + if !strings.Contains(code, "msg.values = new Array(3);") { + t.Error("Missing fixed array allocation in deserialize") + } +} + +func TestGenerateTypeScript_Slices(t *testing.T) { + schema := parser.Schema{ + Messages: []parser.Message{ + { + PackageName: "test", + Name: "SliceMessage", + Fields: []parser.Field{ + { + Name: "Items", + Kind: parser.KindSlice, + Elem: &parser.Field{ + Kind: parser.KindPrimitive, + Primitive: parser.KindInt32, + }, + }, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check default value (using camelCase field name) + if !strings.Contains(code, "items: number[] = [];") { + t.Error("Missing slice field with default") + } + + // Check length prefix in serialize (using camelCase field name) + if !strings.Contains(code, "view.setUint16(pos, this.items.length, true);") { + t.Error("Missing slice length prefix in serialize") + } + + // Check length reading in deserialize + if !strings.Contains(code, "const _lenItems = view.getUint16(pos, true);") { + t.Error("Missing slice length reading in deserialize") + } + + // Check array allocation in deserialize (using camelCase field name) + if !strings.Contains(code, "msg.items = new Array(_lenItems);") { + t.Error("Missing slice allocation in deserialize") + } +} + +func TestGenerateTypeScript_Enums(t *testing.T) { + schema := parser.Schema{ + Enums: []parser.Enum{ + { + Name: "Status", + Primitive: parser.KindUint16, + Values: []parser.EnumValue{ + {Name: "Pending", Value: "0"}, + {Name: "Active", Value: "1"}, + {Name: "Done", Value: "2"}, + }, + }, + }, + Messages: []parser.Message{ + { + PackageName: "test", + Name: "EnumMessage", + Fields: []parser.Field{ + { + Name: "Status", + Kind: parser.KindPrimitive, + Primitive: parser.KindUint16, + NamedType: "Status", + }, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check enum definition + if !strings.Contains(code, "export enum Status {") { + t.Error("Missing enum definition") + } + if !strings.Contains(code, "Pending = 0,") { + t.Error("Missing Pending enum value") + } + if !strings.Contains(code, "Active = 1,") { + t.Error("Missing Active enum value") + } + + // Check enum field type (using camelCase field name) + if !strings.Contains(code, "status: Status = 0;") { + t.Error("Missing enum field with correct type") + } + + // Check enum serialization (cast to number, using camelCase field name) + if !strings.Contains(code, "view.setUint16(pos, this.status as number, true);") { + t.Error("Missing enum cast in serialize") + } + + // Check enum deserialization (cast from number, using camelCase field name) + if !strings.Contains(code, "msg.status = (view.getUint16(pos, true) as Status);") { + t.Error("Missing enum cast in deserialize") + } +} + +func TestGenerateTypeScript_Strings(t *testing.T) { + schema := parser.Schema{ + Messages: []parser.Message{ + { + PackageName: "test", + Name: "StringMessage", + Fields: []parser.Field{ + {Name: "Name", Kind: parser.KindPrimitive, Primitive: parser.KindString}, + }, + }, + }, + } + + src, err := GenerateTypeScriptSchema(schema, "Test") + if err != nil { + t.Fatalf("GenerateTypeScriptSchema: %v", err) + } + + code := string(src) + + // Check TextEncoder usage + if !strings.Contains(code, "new TextEncoder().encode(") { + t.Error("Missing TextEncoder in serialize") + } + + // Check length prefix + if !strings.Contains(code, "view.setUint16(pos, _slen") { + t.Error("Missing string length prefix in serialize") + } + + // Check TextDecoder usage + if !strings.Contains(code, "new TextDecoder().decode(") { + t.Error("Missing TextDecoder in deserialize") + } +}