feat: added support typescript

This commit is contained in:
2026-03-23 16:04:31 +03:00
parent 40b81de08d
commit d41cef5576
7 changed files with 1469 additions and 35 deletions
+97
View File
@@ -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/...
+1 -1
View File
@@ -20,4 +20,4 @@ formatters:
linters-settings:
goimports:
local-prefixes:
- gorena/server
- github.com/edmand46/arpack
+37 -5
View File
@@ -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<number>(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
+24 -4
View File
@@ -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 {
+263 -14
View File
@@ -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,6 +41,14 @@ func TestE2E_CrossLanguage(t *testing.T) {
{"EnvelopeMessage", "EnvelopeMessage", 0},
}
// 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, "")
@@ -63,6 +61,33 @@ func TestE2E_CrossLanguage(t *testing.T) {
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")
}
}
// --- Build helpers ---
@@ -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(`<Project Sdk="Microsoft.NET.Sdk">
</PropertyGroup>
</Project>
`)
// --- 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();
`
+635
View File
@@ -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("// <auto-generated> arpack </auto-generated>\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
}
+401
View File
@@ -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<number>(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")
}
}