feat: added support typescript
This commit is contained in:
@@ -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
@@ -20,4 +20,4 @@ formatters:
|
|||||||
linters-settings:
|
linters-settings:
|
||||||
goimports:
|
goimports:
|
||||||
local-prefixes:
|
local-prefixes:
|
||||||
- gorena/server
|
- github.com/edmand46/arpack
|
||||||
@@ -4,16 +4,19 @@
|
|||||||
|
|
||||||
# ArPack
|
# 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#.
|
[](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
|
## 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
|
- **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)
|
- **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
|
- **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
|
## 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.
|
- **[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.
|
- **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.
|
- **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.
|
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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```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 |
|
| 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) |
|
| `-in` | Input Go file with struct definitions (required) |
|
||||||
| `-out-go` | Output directory for generated Go code |
|
| `-out-go` | Output directory for generated Go code |
|
||||||
| `-out-cs` | Output directory for generated C# code |
|
| `-out-cs` | Output directory for generated C# code |
|
||||||
|
| `-out-ts` | Output directory for generated TypeScript code |
|
||||||
| `-cs-namespace` | C# namespace (default: `Arpack.Messages`) |
|
| `-cs-namespace` | C# namespace (default: `Arpack.Messages`) |
|
||||||
|
|
||||||
**Output files:**
|
**Output files:**
|
||||||
- Go: `{name}_gen.go`
|
- Go: `{name}_gen.go`
|
||||||
- C#: `{Name}.gen.cs`
|
- C#: `{Name}.gen.cs`
|
||||||
|
- TypeScript: `{Name}.gen.ts`
|
||||||
|
|
||||||
## Schema Definition
|
## 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.
|
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
|
## Wire Format
|
||||||
|
|
||||||
- Little-endian byte order
|
- Little-endian byte order
|
||||||
|
|||||||
+24
-4
@@ -1,28 +1,31 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/edmand46/arpack/generator"
|
|
||||||
"github.com/edmand46/arpack/parser"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/edmand46/arpack/generator"
|
||||||
|
"github.com/edmand46/arpack/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
in := flag.String("in", "", "input Go file with struct definitions")
|
in := flag.String("in", "", "input Go file with struct definitions")
|
||||||
outGo := flag.String("out-go", "", "output directory for generated Go code")
|
outGo := flag.String("out-go", "", "output directory for generated Go code")
|
||||||
outCS := flag.String("out-cs", "", "output directory for generated C# 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")
|
namespace := flag.String("cs-namespace", "Arpack.Messages", "C# namespace")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *in == "" {
|
if *in == "" {
|
||||||
log.Fatal("arpack: -in is required")
|
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)
|
schema, err := parser.ParseSchemaFile(*in)
|
||||||
@@ -74,6 +77,23 @@ func main() {
|
|||||||
|
|
||||||
fmt.Printf("arpack: wrote %s\n", outPath)
|
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 {
|
func toTitle(s string) string {
|
||||||
|
|||||||
+274
-25
@@ -2,9 +2,9 @@ package e2e
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"github.com/edmand46/arpack/generator"
|
"github.com/edmand46/arpack/generator"
|
||||||
"github.com/edmand46/arpack/parser"
|
"github.com/edmand46/arpack/parser"
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -16,12 +16,8 @@ import (
|
|||||||
|
|
||||||
const samplePath = "../testdata/sample.go"
|
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) {
|
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)
|
schema, err := parser.ParseSchemaFile(samplePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse: %v", err)
|
t.Fatalf("parse: %v", err)
|
||||||
@@ -32,13 +28,7 @@ func TestE2E_CrossLanguage(t *testing.T) {
|
|||||||
t.Fatalf("GenerateGoSchema: %v", err)
|
t.Fatalf("GenerateGoSchema: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
csSrc, err := generator.GenerateCSharpSchema(schema, "Ragono.Messages")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GenerateCSharpSchema: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
goDir := buildGoHarness(t, goSrc)
|
goDir := buildGoHarness(t, goSrc)
|
||||||
csDir := buildCSHarness(t, csSrc)
|
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -51,17 +41,52 @@ func TestE2E_CrossLanguage(t *testing.T) {
|
|||||||
{"EnvelopeMessage", "EnvelopeMessage", 0},
|
{"EnvelopeMessage", "EnvelopeMessage", 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
// C# tests (if dotnet is available)
|
||||||
t.Run("Go_to_CS/"+tc.name, func(t *testing.T) {
|
if _, err := exec.LookPath("dotnet"); err == nil {
|
||||||
hex := runHarness(t, goDir, "go", "ser", tc.typ, "")
|
csSrc, err := generator.GenerateCSharpSchema(schema, "Ragono.Messages")
|
||||||
out := runHarness(t, csDir, "cs", "deser", tc.typ, hex)
|
if err != nil {
|
||||||
checkOutput(t, tc.typ, out, tc.epsilon)
|
t.Fatalf("GenerateCSharpSchema: %v", err)
|
||||||
})
|
}
|
||||||
t.Run("CS_to_Go/"+tc.name, func(t *testing.T) {
|
csDir := buildCSHarness(t, csSrc)
|
||||||
hex := runHarness(t, csDir, "cs", "ser", tc.typ, "")
|
|
||||||
out := runHarness(t, goDir, "go", "deser", tc.typ, hex)
|
for _, tc := range cases {
|
||||||
checkOutput(t, tc.typ, out, tc.epsilon)
|
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
|
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 ---
|
// --- Harness runners ---
|
||||||
|
|
||||||
func runHarness(t *testing.T, dir, lang, op, typ, hexInput string) string {
|
func runHarness(t *testing.T, dir, lang, op, typ, hexInput string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if lang == "go" {
|
switch lang {
|
||||||
|
case "go":
|
||||||
args := []string{op, typ}
|
args := []string{op, typ}
|
||||||
if hexInput != "" {
|
if hexInput != "" {
|
||||||
args = append(args, hexInput)
|
args = append(args, hexInput)
|
||||||
}
|
}
|
||||||
cmd = exec.Command(filepath.Join(dir, "harness"), args...)
|
cmd = exec.Command(filepath.Join(dir, "harness"), args...)
|
||||||
} else {
|
case "cs":
|
||||||
args := []string{op, typ}
|
args := []string{op, typ}
|
||||||
if hexInput != "" {
|
if hexInput != "" {
|
||||||
args = append(args, hexInput)
|
args = append(args, hexInput)
|
||||||
}
|
}
|
||||||
cmd = exec.Command("dotnet", append([]string{filepath.Join(dir, "out", "E2EHarness.dll")}, args...)...)
|
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
|
cmd.Dir = dir
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
@@ -519,3 +580,191 @@ var csProjSource = fmt.Sprintf(`<Project Sdk="Microsoft.NET.Sdk">
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</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
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user