├── packed_test.ts ├── testdata ├── empty-1-message.msg.bin ├── empty-1-message.msg.txt ├── fields-1-sub.msg.bin ├── rpc1-1-myresponse.msg.bin ├── empty-1-message.msg.json ├── fields-1-sub.msg.txt ├── rpc1-1-myrequest.msg.bin ├── importee-1-importee.msg.bin ├── rpc1-1-myresponse.msg.txt ├── importee-1-importee.msg.txt ├── importer-1-importer.msg.bin ├── rpc1-1-myrequest.msg.txt ├── rpc1-3-myresponse.msg.txt ├── importee-1-importee.msg.json ├── rpc1-1-myrequest.msg.json ├── rpc1-1-myresponse.msg.json ├── rpc1-2-myresponse.msg.txt ├── rpc1-3-myresponse.msg.json ├── fields-1-sub.msg.json ├── deps.ts ├── rpc1-2-myresponse.msg.json ├── empty.proto ├── rpc1-2-myrequest.msg.bin ├── rpc1-2-myrequest.msg.txt ├── rpc1-2-myrequest.msg.json ├── importer-1-importer.msg.txt ├── fields-1-fields.msg.bin ├── importer-1-importer.msg.json ├── rpc1-2-myresponse.msg.bin ├── rpc1-3-myresponse.msg.bin ├── importee.proto ├── importer.proto ├── empty.pb.ts ├── rpc1.proto ├── fields.proto ├── fields-1-fields.msg.txt ├── fields-1-fields.msg.json ├── importee.pb.ts ├── importer.pb.ts ├── rpc1.pb.ts └── fields.pb.ts ├── .tool-versions ├── version.ts ├── .gitignore ├── package.json ├── protobufentry.ts ├── tsconfig.json ├── concat.ts ├── deps.ts ├── .github └── workflows │ └── build.yml ├── bool.ts ├── string.ts ├── from_json.ts ├── to_json.ts ├── float.ts ├── int64.ts ├── uint64.ts ├── int32.ts ├── fixed64.ts ├── uint32.ts ├── sfixed64.ts ├── double.ts ├── sfixed32.ts ├── to_bytes.ts ├── bytes.ts ├── sint64.ts ├── fixed32.ts ├── sint32.ts ├── enum.ts ├── mod.ts ├── serialize.ts ├── string_test.ts ├── from_bytes.ts ├── deserialize.ts ├── map_test.ts ├── protod.ts ├── repeated.ts ├── packed.ts ├── README.md ├── types.ts ├── map.ts ├── generate_test.ts ├── serialize_test.ts └── generate.ts /packed_test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/empty-1-message.msg.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/empty-1-message.msg.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/fields-1-sub.msg.bin: -------------------------------------------------------------------------------- 1 | 2 | foo -------------------------------------------------------------------------------- /testdata/rpc1-1-myresponse.msg.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /testdata/empty-1-message.msg.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testdata/fields-1-sub.msg.txt: -------------------------------------------------------------------------------- 1 | a: "foo" 2 | -------------------------------------------------------------------------------- /testdata/rpc1-1-myrequest.msg.bin: -------------------------------------------------------------------------------- 1 | 2 | foo -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | deno 1.20.4 2 | protoc 3.11.0 3 | -------------------------------------------------------------------------------- /testdata/importee-1-importee.msg.bin: -------------------------------------------------------------------------------- 1 | 2 | bar -------------------------------------------------------------------------------- /testdata/rpc1-1-myresponse.msg.txt: -------------------------------------------------------------------------------- 1 | status: 1 2 | -------------------------------------------------------------------------------- /version.ts: -------------------------------------------------------------------------------- 1 | export const version = "0.3.3"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /testdata/importee-1-importee.msg.txt: -------------------------------------------------------------------------------- 1 | foo: "bar" 2 | -------------------------------------------------------------------------------- /testdata/importer-1-importer.msg.bin: -------------------------------------------------------------------------------- 1 | 2 |  3 | bar -------------------------------------------------------------------------------- /testdata/rpc1-1-myrequest.msg.txt: -------------------------------------------------------------------------------- 1 | path: "foo" 2 | -------------------------------------------------------------------------------- /testdata/rpc1-3-myresponse.msg.txt: -------------------------------------------------------------------------------- 1 | status: -1 2 | -------------------------------------------------------------------------------- /testdata/importee-1-importee.msg.json: -------------------------------------------------------------------------------- 1 | { "foo": "bar" } 2 | -------------------------------------------------------------------------------- /testdata/rpc1-1-myrequest.msg.json: -------------------------------------------------------------------------------- 1 | { "path": "foo" } 2 | -------------------------------------------------------------------------------- /testdata/rpc1-1-myresponse.msg.json: -------------------------------------------------------------------------------- 1 | { "status": 1 } 2 | -------------------------------------------------------------------------------- /testdata/rpc1-2-myresponse.msg.txt: -------------------------------------------------------------------------------- 1 | status: 2147483647 2 | -------------------------------------------------------------------------------- /testdata/rpc1-3-myresponse.msg.json: -------------------------------------------------------------------------------- 1 | { "status": -1 } 2 | -------------------------------------------------------------------------------- /testdata/fields-1-sub.msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "foo" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/deps.ts: -------------------------------------------------------------------------------- 1 | export { Importee } from "./importee.pb.ts"; 2 | -------------------------------------------------------------------------------- /testdata/rpc1-2-myresponse.msg.json: -------------------------------------------------------------------------------- 1 | { "status": 2147483647 } 2 | -------------------------------------------------------------------------------- /testdata/empty.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Message {} 4 | -------------------------------------------------------------------------------- /testdata/rpc1-2-myrequest.msg.bin: -------------------------------------------------------------------------------- 1 | 2 | 1<-9223372036854775808/-1Ω≈ç√∫˜µ≤≥÷ -------------------------------------------------------------------------------- /testdata/rpc1-2-myrequest.msg.txt: -------------------------------------------------------------------------------- 1 | path: "<-9223372036854775808/-1Ω≈ç√∫˜µ≤≥÷" 2 | -------------------------------------------------------------------------------- /testdata/rpc1-2-myrequest.msg.json: -------------------------------------------------------------------------------- 1 | { "path": "<-9223372036854775808/-1Ω≈ç√∫˜µ≤≥÷" } 2 | -------------------------------------------------------------------------------- /testdata/importer-1-importer.msg.txt: -------------------------------------------------------------------------------- 1 | msg: { 2 | foo: "bar" 3 | } 4 | eeenumjenkins: VALID 5 | -------------------------------------------------------------------------------- /testdata/fields-1-fields.msg.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithamus/deno-protod/HEAD/testdata/fields-1-fields.msg.bin -------------------------------------------------------------------------------- /testdata/importer-1-importer.msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": { 3 | "foo": "bar" 4 | }, 5 | "eeenumjenkins": "VALID" 6 | } 7 | -------------------------------------------------------------------------------- /testdata/rpc1-2-myresponse.msg.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithamus/deno-protod/HEAD/testdata/rpc1-2-myresponse.msg.bin -------------------------------------------------------------------------------- /testdata/rpc1-3-myresponse.msg.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithamus/deno-protod/HEAD/testdata/rpc1-3-myresponse.msg.bin -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "typescript": "^3.9.7", 4 | "typescript-deno-plugin": "^1.31.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /protobufentry.ts: -------------------------------------------------------------------------------- 1 | export type ProtoBufEntry = 2 | | [number, 0, bigint] 3 | | [number, 1, Uint8Array] 4 | | [number, 2, Uint8Array] 5 | | [number, 5, Uint8Array]; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "noEmit": true, 5 | "plugins": [{ "name": "typescript-deno-plugin" }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testdata/importee.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Importee { 4 | string foo = 1; 5 | } 6 | 7 | enum Importeenum { 8 | INVALID = 0; 9 | VALID = 1; 10 | } 11 | -------------------------------------------------------------------------------- /testdata/importer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "importee.proto"; 4 | 5 | message Importer { 6 | Importee msg = 1; 7 | Importeenum eeenumjenkins = 2; 8 | } 9 | -------------------------------------------------------------------------------- /concat.ts: -------------------------------------------------------------------------------- 1 | export function concat(a: Uint8Array, b: Uint8Array): Uint8Array { 2 | const sum = new Uint8Array(a.length + b.length); 3 | sum.set(a, 0); 4 | sum.set(b, a.length); 5 | return sum; 6 | } 7 | -------------------------------------------------------------------------------- /testdata/empty.pb.ts: -------------------------------------------------------------------------------- 1 | // Generated by protod v0.2.1 2 | 3 | export class Message { 4 | constructor() {} 5 | 6 | static fields = {}; 7 | 8 | static fromBytes(): Message { 9 | return new Message(); 10 | } 11 | 12 | static fromJSON(): Message { 13 | return new Message(); 14 | } 15 | 16 | toBytes(): Uint8Array { 17 | return Uint8Array.of(); 18 | } 19 | 20 | toJSON() { 21 | return {}; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Enum, 3 | EnumField, 4 | Field, 5 | Import, 6 | MapField, 7 | Message, 8 | Oneof, 9 | Option, 10 | parse, 11 | Proto, 12 | Syntax, 13 | } from "https://deno.land/x/protoc_parser@v0.2.9/mod.ts"; 14 | 15 | export type { Visitor } from "https://deno.land/x/protoc_parser@v0.2.9/mod.ts"; 16 | 17 | export { decode, encode } from "https://deno.land/x/varint@v2.0.0/varint.ts"; 18 | 19 | export { dirname, join } from "https://deno.land/x/std@0.141.0/path/mod.ts"; 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Unit Tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the project 9 | uses: actions/checkout@v2 10 | - name: Setup Deno 11 | uses: denolib/setup-deno@v2 12 | with: 13 | deno-version: v1.20.4 14 | - name: Fmt 15 | run: deno fmt --check 16 | - name: Lint 17 | run: deno lint --unstable 18 | - name: Test 19 | run: deno test --allow-read 20 | -------------------------------------------------------------------------------- /bool.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf boolean fields. 5 | */ 6 | export const boolField: FieldTypeVarint = { 7 | name: "bool", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): boolean { 11 | return value === 1n; 12 | }, 13 | 14 | fromJSON(value: NonNullable): boolean { 15 | if (typeof value === "boolean") return value; 16 | throw new TypeError(`malformed json`); 17 | }, 18 | 19 | toBytes(value: boolean): bigint { 20 | return value ? 1n : 0n; 21 | }, 22 | 23 | toJSON(value: boolean): boolean { 24 | return value; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /testdata/rpc1.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a generic file comment 3 | */ 4 | syntax = "proto3"; 5 | 6 | /** 7 | * This is a comment for MyService 8 | */ 9 | service MyService { 10 | 11 | /** 12 | * This is a comment for MyMethod 13 | */ 14 | rpc MyMethod (MyRequest) returns (MyResponse); 15 | } 16 | 17 | /** 18 | * This is a comment for MyRequest 19 | */ 20 | message MyRequest { 21 | /** 22 | * This is a comment for path 23 | */ 24 | string path = 1; 25 | } 26 | 27 | /** 28 | * This is a comment for MyResponse 29 | */ 30 | message MyResponse { 31 | /** 32 | * This is a comment for status 33 | */ 34 | int32 status = 2; 35 | } 36 | -------------------------------------------------------------------------------- /string.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeLengthDelimited, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf String Fields. 5 | */ 6 | export const stringField: FieldTypeLengthDelimited = { 7 | name: "string", 8 | wireType: 2, 9 | 10 | fromBytes(value: Uint8Array): string { 11 | return new TextDecoder("utf-8").decode(value) || ""; 12 | }, 13 | 14 | fromJSON(value: NonNullable): string { 15 | if (typeof value == "string") return value; 16 | throw new TypeError(`malformed json`); 17 | }, 18 | 19 | toBytes(value: string): Uint8Array { 20 | return new TextEncoder().encode(value); 21 | }, 22 | 23 | toJSON(value: string): string { 24 | return value; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /from_json.ts: -------------------------------------------------------------------------------- 1 | import type { FieldSet, JSON } from "./types.ts"; 2 | 3 | /** 4 | * Given arbitrary JSON, and a set of fields to extract with their respective 5 | * helper functions, this will create a JavaScript object ready to be consumed 6 | * by a Message class. 7 | */ 8 | export function fromJSON(json: JSON, set: FieldSet): Partial { 9 | if (typeof json !== "object" || !json || Array.isArray(json)) { 10 | throw new TypeError("malformed json"); 11 | } 12 | const init: Partial = {}; 13 | for (const key in set) { 14 | const field = set[key]![1]; 15 | const value = json[key as string]; 16 | if (value != null) { 17 | init[key as keyof T] = field.fromJSON(value); 18 | } 19 | } 20 | return init; 21 | } 22 | -------------------------------------------------------------------------------- /to_json.ts: -------------------------------------------------------------------------------- 1 | import type { FieldSet, JSON, MessageInstance } from "./types.ts"; 2 | 3 | type JSONAble = { 4 | [P in keyof T]?: JSON; 5 | }; 6 | 7 | /** 8 | * Given a Message class, and a set of fields to extract with their respective 9 | * helper functions, this will create a JavaScript object ready to be consumed 10 | * into a JSON string. 11 | */ 12 | export function toJSON(fields: T, set: FieldSet): JSONAble { 13 | const json: JSONAble = {}; 14 | for (const key in set) { 15 | const field = set[key]![1]; 16 | if ("wireType" in field) { 17 | const value = field.toJSON(fields[key]); 18 | if (value !== undefined) json[key] = value; 19 | } else if ("toJSON" in fields[key]) { 20 | json[key] = (fields[key] as unknown as MessageInstance).toJSON(); 21 | } 22 | } 23 | return json; 24 | } 25 | -------------------------------------------------------------------------------- /testdata/fields.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package fields; 4 | 5 | enum Enum { 6 | a = 0; 7 | b = 1; 8 | c = 2; 9 | } 10 | 11 | message Sub { 12 | string a = 1; 13 | } 14 | 15 | message Fields { 16 | int32 a = 1; 17 | int64 b = 2; 18 | uint32 c = 3; 19 | uint64 d = 4; 20 | sint32 e = 5; 21 | sint64 f = 6; 22 | bool g = 7; 23 | Enum h = 8; 24 | fixed64 i = 9; 25 | sfixed64 j = 10; 26 | double k = 11; 27 | string l = 12; 28 | bytes m = 13; 29 | Sub n = 14; 30 | fixed32 o = 15; 31 | sfixed32 p = 16; 32 | map q = 17; 33 | map r = 18; 34 | map s = 19; 35 | map t = 20; 36 | repeated uint64 u = 21; 37 | repeated bool v = 22; 38 | repeated string w = 23; 39 | repeated Sub x = 24; 40 | oneof yorz { 41 | string y = 25; 42 | int32 z = 26; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /testdata/fields-1-fields.msg.txt: -------------------------------------------------------------------------------- 1 | a: -2147483647 2 | b: -9223372036854775808 3 | c: 4294967295 4 | d: 18446744073709551615 5 | e: -2147483647 6 | f: -9223372036854775808 7 | g: true 8 | h: 1 9 | i: 18446744073709551615 10 | j: -9223372036854775808 11 | k: 1.7976931348623157e+308 12 | l: "foo" 13 | m: "\x66\x66\x66\x66\x66\x66" 14 | n: { 15 | a: "foo" 16 | } 17 | o: 4294967295 18 | p: -2147483647 19 | q { 20 | key: -2147483647 21 | value: -2147483647 22 | } 23 | r: { 24 | key: -2147483647 25 | value: "foo" 26 | } 27 | s: { 28 | key: "foo" 29 | value: -2147483647 30 | } 31 | s: { 32 | key: "bar" 33 | value: 2147483647 34 | } 35 | t: { 36 | key: 18446744073709551615 37 | value: 2 38 | } 39 | u: [18446744073709551615, 18446744073709551615, 18446744073709551615] 40 | v: [false, true, false] 41 | w: ["foo", "bar", "baz"] 42 | x: [ 43 | { a: "foo" }, 44 | { a: "bar" } 45 | ] 46 | y: "foo" 47 | -------------------------------------------------------------------------------- /float.ts: -------------------------------------------------------------------------------- 1 | import { FieldType32Bit, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Float fields. 5 | */ 6 | export const floatField: FieldType32Bit = { 7 | name: "float", 8 | wireType: 5, 9 | 10 | fromBytes(value: Uint8Array): number { 11 | return new DataView(value.buffer).getFloat32(0, true); 12 | }, 13 | 14 | fromJSON(value: NonNullable): number { 15 | const num = Number(value); 16 | if (typeof value == "number") { 17 | return value; 18 | } else if (typeof value == "string" && Number.isNaN(num)) { 19 | return num; 20 | } 21 | throw new TypeError(`malformed json`); 22 | }, 23 | 24 | toBytes(value: number): Uint8Array { 25 | const buf = new Uint8Array(8); 26 | new DataView(buf.buffer).setFloat32(0, value, true); 27 | return buf; 28 | }, 29 | 30 | toJSON(value: number): number { 31 | return value; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /int64.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Int64 fields. 5 | */ 6 | export const int64Field: FieldTypeVarint = { 7 | name: "int64", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): bigint { 11 | return BigInt.asIntN(64, value); 12 | }, 13 | 14 | fromJSON(value: NonNullable): bigint { 15 | if (typeof value == "string") { 16 | return BigInt(value); 17 | } 18 | if (typeof value == "bigint" && BigInt.asIntN(64, value) === value) { 19 | return value; 20 | } 21 | if (typeof value == "number" && Number.isInteger(value)) { 22 | return BigInt(value); 23 | } 24 | throw new TypeError(`malformed json`); 25 | }, 26 | 27 | toBytes(value: bigint): bigint { 28 | return BigInt.asUintN(64, value); 29 | }, 30 | 31 | toJSON(value: bigint): string { 32 | return String(value); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /uint64.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Unsigned Uint32 Fields. 5 | */ 6 | export const uint64Field: FieldTypeVarint = { 7 | name: "uint64", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): bigint { 11 | return BigInt.asUintN(64, value); 12 | }, 13 | 14 | fromJSON(value: NonNullable): bigint { 15 | if (typeof value == "string") { 16 | return BigInt(value); 17 | } 18 | if (typeof value == "bigint" && BigInt.asUintN(64, value) === value) { 19 | return value; 20 | } 21 | if (typeof value == "number" && Number.isInteger(value)) { 22 | return BigInt(value); 23 | } 24 | throw new TypeError(`malformed json`); 25 | }, 26 | 27 | toBytes(value: bigint): bigint { 28 | return BigInt.asUintN(64, value); 29 | }, 30 | 31 | toJSON(value: bigint): string { 32 | return String(value); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /testdata/fields-1-fields.msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": -2147483647, 3 | "b": "-9223372036854775808", 4 | "c": 4294967295, 5 | "d": "18446744073709551615", 6 | "e": -2147483647, 7 | "f": "-9223372036854775808", 8 | "g": true, 9 | "h": "b", 10 | "i": "18446744073709551615", 11 | "j": "-9223372036854775808", 12 | "k": 1.7976931348623157e+308, 13 | "l": "foo", 14 | "m": "ZmZmZmZm", 15 | "n": { 16 | "a": "foo" 17 | }, 18 | "o": 4294967295, 19 | "p": -2147483647, 20 | "q": { 21 | "-2147483647": -2147483647 22 | }, 23 | "r": { 24 | "-2147483647": "foo" 25 | }, 26 | "s": { 27 | "foo": -2147483647, 28 | "bar": 2147483647 29 | }, 30 | "t": { 31 | "18446744073709551615": "c" 32 | }, 33 | "u": ["18446744073709551615", "18446744073709551615", "18446744073709551615"], 34 | "v": [false, true, false], 35 | "w": ["foo", "bar", "baz"], 36 | "x": [{ "a": "foo" }, { "a": "bar" }], 37 | "y": "foo" 38 | } 39 | -------------------------------------------------------------------------------- /int32.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Int32 fields. 5 | */ 6 | export const int32Field: FieldTypeVarint = { 7 | name: "int32", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): number { 11 | return Number(BigInt.asIntN(32, value)); 12 | }, 13 | 14 | fromJSON(value: NonNullable): number { 15 | if (typeof value == "string" && Number.isInteger(Number(value))) { 16 | return Number(value); 17 | } 18 | if (typeof value == "bigint" && BigInt.asIntN(32, value) === value) { 19 | return Number(value); 20 | } 21 | if (typeof value == "number" && Number.isInteger(value)) { 22 | return value; 23 | } 24 | throw new TypeError(`malformed json`); 25 | }, 26 | 27 | toBytes(value: number): bigint { 28 | return BigInt.asUintN(64, BigInt(value)); 29 | }, 30 | 31 | toJSON(value: number): number { 32 | return value; 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /testdata/importee.pb.ts: -------------------------------------------------------------------------------- 1 | // Generated by protod v0.1.3 2 | import { 3 | FieldSet, 4 | fromBytes, 5 | fromJSON, 6 | JSON, 7 | stringField, 8 | toBytes, 9 | toJSON, 10 | } from "../mod.ts"; 11 | 12 | export enum Importeenum { 13 | INVALID = 0, 14 | VALID = 1, 15 | } 16 | 17 | export class Importee { 18 | foo: string; 19 | 20 | constructor(init: Partial) { 21 | this.foo = init.foo ?? ""; 22 | } 23 | 24 | static fields: FieldSet = { 25 | foo: [1, stringField], 26 | }; 27 | 28 | static fromBytes(bytes: Uint8Array): Importee { 29 | return new Importee( 30 | fromBytes(bytes, Importee.fields), 31 | ); 32 | } 33 | 34 | static fromJSON(json: JSON): Importee { 35 | return new Importee( 36 | fromJSON(json, Importee.fields), 37 | ); 38 | } 39 | 40 | toBytes(): Uint8Array { 41 | return toBytes(this, Importee.fields); 42 | } 43 | 44 | toJSON() { 45 | return toJSON(this, Importee.fields); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fixed64.ts: -------------------------------------------------------------------------------- 1 | import { FieldType64Bit, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Fixed64 fields. 5 | */ 6 | export const fixed64Field: FieldType64Bit = { 7 | name: "fixed64", 8 | wireType: 1, 9 | 10 | fromBytes(value: Uint8Array): bigint { 11 | return new DataView(value.buffer).getBigUint64(0, true); 12 | }, 13 | 14 | fromJSON(value: NonNullable): bigint { 15 | if (typeof value == "string") { 16 | return BigInt(value); 17 | } 18 | if (typeof value == "bigint" && BigInt.asUintN(64, value) === value) { 19 | return value; 20 | } 21 | if (typeof value == "number" && Number.isInteger(value)) { 22 | return BigInt(value); 23 | } 24 | throw new TypeError(`malformed json`); 25 | }, 26 | 27 | toBytes(value: bigint): Uint8Array { 28 | const buf = new Uint8Array(8); 29 | new DataView(buf.buffer).setBigUint64(0, value, true); 30 | return buf; 31 | }, 32 | 33 | toJSON(value: bigint): string { 34 | return String(value); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /uint32.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Unsigned Int32 Fields. 5 | */ 6 | export const uint32Field: FieldTypeVarint = { 7 | name: "uint32", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): number { 11 | return Number(BigInt.asUintN(32, value)); 12 | }, 13 | 14 | fromJSON(value: NonNullable): number { 15 | const num = Number(value); 16 | if (typeof value == "string" && Number.isInteger(num) && num >= 0) { 17 | return num; 18 | } 19 | if ( 20 | typeof value == "bigint" && BigInt.asIntN(32, value) === value && num >= 0 21 | ) { 22 | return num; 23 | } 24 | if (typeof value == "number" && Number.isInteger(value) && num >= 0) { 25 | return num; 26 | } 27 | throw new TypeError(`malformed json`); 28 | }, 29 | 30 | toBytes(value: number): bigint { 31 | return BigInt.asUintN(32, BigInt(value)); 32 | }, 33 | 34 | toJSON(value: number): number { 35 | return value; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /sfixed64.ts: -------------------------------------------------------------------------------- 1 | import { FieldType64Bit, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Signed Fixed32 fields. 5 | */ 6 | export const sfixed64Field: FieldType64Bit = { 7 | name: "sfixed64", 8 | wireType: 1, 9 | 10 | fromBytes(value: Uint8Array): bigint { 11 | return new DataView(value.buffer).getBigInt64(0, true); 12 | }, 13 | 14 | fromJSON(value: NonNullable): bigint { 15 | if (typeof value == "string") { 16 | return BigInt(value); 17 | } 18 | if (typeof value == "bigint" && BigInt.asIntN(64, value) === value) { 19 | return value; 20 | } 21 | if (typeof value == "number" && Number.isInteger(value)) { 22 | return BigInt(value); 23 | } 24 | throw new TypeError(`malformed json`); 25 | }, 26 | 27 | toBytes(value: bigint): Uint8Array { 28 | const buf = new Uint8Array(8); 29 | new DataView(buf.buffer).setBigInt64(0, value, true); 30 | return buf; 31 | }, 32 | 33 | toJSON(value: bigint): string { 34 | return String(value); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /double.ts: -------------------------------------------------------------------------------- 1 | import { FieldType64Bit, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Double fields (aka JavaScript Numbers). 5 | */ 6 | export const doubleField: FieldType64Bit = { 7 | name: "double", 8 | wireType: 1, 9 | 10 | fromBytes(value: Uint8Array): number { 11 | return new DataView(value.buffer).getFloat64(0, true); 12 | }, 13 | 14 | fromJSON(value: NonNullable): number { 15 | const num = Number(value); 16 | if (typeof value == "number" && !Number.isNaN(value)) { 17 | return num; 18 | } else if (typeof value == "string" && !Number.isNaN(num)) { 19 | return num; 20 | } else if (typeof value == "bigint" && String(value) === String(num)) { 21 | return num; 22 | } 23 | throw new TypeError(`malformed json`); 24 | }, 25 | 26 | toBytes(value: number): Uint8Array { 27 | const buf = new Uint8Array(8); 28 | new DataView(buf.buffer).setFloat64(0, value, true); 29 | return buf; 30 | }, 31 | 32 | toJSON(value: number): number { 33 | return value; 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /sfixed32.ts: -------------------------------------------------------------------------------- 1 | import { FieldType32Bit, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Signed Fixed32 fields. 5 | */ 6 | export const sfixed32Field: FieldType32Bit = { 7 | name: "sfixed32", 8 | wireType: 5, 9 | 10 | fromBytes(value: Uint8Array): number { 11 | return new DataView(value.buffer).getInt32(0, true); 12 | }, 13 | 14 | fromJSON(value: NonNullable): number { 15 | const num = Number(value); 16 | if (typeof value == "string" && Number.isInteger(num)) { 17 | return num; 18 | } 19 | if (typeof value == "bigint" && BigInt.asIntN(32, value) === value) { 20 | return num; 21 | } 22 | if (typeof value == "number" && Number.isInteger(value)) { 23 | return num; 24 | } 25 | throw new TypeError(`malformed json`); 26 | }, 27 | 28 | toBytes(value: number): Uint8Array { 29 | const buf = new Uint8Array(8); 30 | new DataView(buf.buffer).setInt32(0, value, true); 31 | return buf; 32 | }, 33 | 34 | toJSON(value: number): number { 35 | return value; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /to_bytes.ts: -------------------------------------------------------------------------------- 1 | import type { FieldSet, MessageInstance, ProtoBufEntry } from "./types.ts"; 2 | import { serialize } from "./serialize.ts"; 3 | 4 | /** 5 | * Given a Message class, and a set of fields to extract with their respective 6 | * helper functions, this will serialize the Message into the ProtoBuf binary 7 | * format. 8 | */ 9 | export function toBytes(fields: T, set: FieldSet): Uint8Array { 10 | let values: ProtoBufEntry[] = []; 11 | for (const key in set) { 12 | const [id, field] = set[key]!; 13 | const val = fields[key]; 14 | if (val === undefined) continue; 15 | if ("wireType" in field && "toEntry" in field) { 16 | values = values.concat(field.toEntry(id, val)); 17 | } else if ("wireType" in field && "toBytes" in field) { 18 | values.push([id, field.wireType, field.toBytes(val)] as ProtoBufEntry); 19 | } else if (fields[key] instanceof field) { 20 | const message = (fields[key] as unknown as MessageInstance); 21 | values.push([id, 2, message.toBytes()]); 22 | } 23 | } 24 | return serialize(values); 25 | } 26 | -------------------------------------------------------------------------------- /bytes.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeLengthDelimited, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Bytes fields. 5 | */ 6 | export const bytesField: FieldTypeLengthDelimited = { 7 | name: "bytes", 8 | wireType: 2, 9 | 10 | fromBytes(value: Uint8Array): Uint8Array { 11 | return value; 12 | }, 13 | 14 | fromJSON(value: NonNullable): Uint8Array { 15 | if (typeof value == "string") { 16 | const str = atob( 17 | value.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"), 18 | ); 19 | const bin = new Uint8Array(str.length); 20 | for (let i = 0; i < bin.length; i += 1) { 21 | bin[i] = str.charCodeAt(i); 22 | } 23 | return bin; 24 | } 25 | throw new TypeError(`malformed json`); 26 | }, 27 | 28 | toBytes(value: Uint8Array): Uint8Array { 29 | return value; 30 | }, 31 | 32 | toJSON(value: Uint8Array): string { 33 | let str = ""; 34 | for (let i = 0; i < value.length; i += 1) { 35 | str += String.fromCharCode(value[i]); 36 | } 37 | return btoa(str); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /sint64.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Signed (ZigZag encoded) Int64 Fields. 5 | */ 6 | export const sint64Field: FieldTypeVarint = { 7 | name: "sint64", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): bigint { 11 | let sint = BigInt.asUintN(64, value) >> 1n; 12 | if ((value & 1n) !== 0n) sint = ~sint; 13 | return BigInt.asIntN(64, sint); 14 | }, 15 | 16 | fromJSON(value: NonNullable): bigint { 17 | if (typeof value == "string") { 18 | return BigInt(value); 19 | } 20 | if (typeof value == "bigint" && BigInt.asIntN(64, value) === value) { 21 | return value; 22 | } 23 | if (typeof value == "number" && Number.isInteger(value)) { 24 | return BigInt(value); 25 | } 26 | throw new TypeError(`malformed json`); 27 | }, 28 | 29 | toBytes(value: bigint): bigint { 30 | let int = BigInt(value) << 1n; 31 | if (value < 0) int = ~int; 32 | return BigInt.asUintN(64, int); 33 | }, 34 | 35 | toJSON(value: bigint): string { 36 | return String(value); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /fixed32.ts: -------------------------------------------------------------------------------- 1 | import { FieldType32Bit, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Fixed32 fields. 5 | */ 6 | export const fixed32Field: FieldType32Bit = { 7 | name: "fixed32", 8 | wireType: 5, 9 | 10 | fromBytes(value: Uint8Array): number { 11 | return new DataView(value.buffer).getUint32(0, true); 12 | }, 13 | 14 | fromJSON(value: NonNullable): number { 15 | const num = Number(value); 16 | if (typeof value == "string" && Number.isInteger(num) && num >= 0) { 17 | return num; 18 | } 19 | if ( 20 | typeof value == "bigint" && BigInt.asIntN(32, value) === value && num >= 0 21 | ) { 22 | return num; 23 | } 24 | if (typeof value == "number" && Number.isInteger(value) && num >= 0) { 25 | return num; 26 | } 27 | throw new TypeError(`malformed json`); 28 | }, 29 | 30 | toBytes(value: number): Uint8Array { 31 | const buf = new Uint8Array(8); 32 | new DataView(buf.buffer).setUint32(0, value, true); 33 | return buf; 34 | }, 35 | 36 | toJSON(value: number): number { 37 | return value; 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /sint32.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Signed (ZigZag encoded) Int32 Fields. 5 | */ 6 | export const sint32Field: FieldTypeVarint = { 7 | name: "sint32", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): number { 11 | let int = BigInt.asUintN(64, value) >> 1n; 12 | if ((value & 1n) !== 0n) int = ~int; 13 | return Number(BigInt.asIntN(32, int)); 14 | }, 15 | 16 | fromJSON(value: NonNullable): number { 17 | const num = Number(value); 18 | if (typeof value == "number" && Number.isInteger(num)) { 19 | return num; 20 | } else if (typeof value == "string" && Number.isInteger(num)) { 21 | return num; 22 | } else if (typeof value == "bigint" && BigInt.asIntN(32, value) === value) { 23 | return num; 24 | } 25 | throw new TypeError(`malformed json`); 26 | }, 27 | 28 | toBytes(value: number): bigint { 29 | let int = BigInt(value) << 1n; 30 | if (value < 0) int = ~int; 31 | return BigInt.asUintN(32, int); 32 | }, 33 | 34 | toJSON(value: number): number { 35 | return value; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /testdata/importer.pb.ts: -------------------------------------------------------------------------------- 1 | // Generated by protod v0.1.3 2 | import { 3 | enumField, 4 | FieldSet, 5 | fromBytes, 6 | fromJSON, 7 | JSON, 8 | toBytes, 9 | toJSON, 10 | } from "../mod.ts"; 11 | import { Importee, Importeenum } from "./importee.pb.ts"; 12 | 13 | export class Importer { 14 | msg: Importee; 15 | eeenumjenkins: Importeenum; 16 | 17 | constructor(init: Partial) { 18 | this.msg = init.msg ?? new Importee({}); 19 | this.eeenumjenkins = init.eeenumjenkins ?? 0; 20 | } 21 | 22 | static fields: FieldSet = { 23 | msg: [1, Importee], 24 | eeenumjenkins: [2, enumField(Importeenum)], 25 | }; 26 | 27 | static fromBytes(bytes: Uint8Array): Importer { 28 | return new Importer( 29 | fromBytes(bytes, Importer.fields), 30 | ); 31 | } 32 | 33 | static fromJSON(json: JSON): Importer { 34 | return new Importer( 35 | fromJSON(json, Importer.fields), 36 | ); 37 | } 38 | 39 | toBytes(): Uint8Array { 40 | return toBytes(this, Importer.fields); 41 | } 42 | 43 | toJSON() { 44 | return toJSON(this, Importer.fields); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /enum.ts: -------------------------------------------------------------------------------- 1 | import { FieldTypeVarint, JSON } from "./types.ts"; 2 | 3 | /** 4 | * A helper object for ProtoBuf Enum fields. 5 | */ 6 | export const enumField = (Enum: T): FieldTypeVarint => ({ 7 | name: "enum", 8 | wireType: 0, 9 | 10 | fromBytes(value: bigint): T[keyof T] { 11 | const int = Number(value); 12 | const key = Enum[int as keyof T]; 13 | if (typeof int === "number" && typeof key === "string") { 14 | return int as unknown as T[keyof T]; 15 | } 16 | return 0 as unknown as T[keyof T]; 17 | }, 18 | 19 | fromJSON(value: NonNullable): T[keyof T] { 20 | const key = Enum[value as keyof T]; 21 | if (typeof value === "number" && typeof key === "string") { 22 | return value as unknown as T[keyof T]; 23 | } 24 | if (typeof value === "string" && typeof key === "number") { 25 | return key as unknown as T[keyof T]; 26 | } 27 | throw new TypeError(`malformed json`); 28 | }, 29 | 30 | toBytes(value: T[keyof T]): bigint { 31 | return BigInt.asUintN(64, BigInt(value as unknown as number)); 32 | }, 33 | 34 | toJSON(value: T[keyof T]): string { 35 | return String(Enum[value as unknown as keyof T]); 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { serialize } from "./serialize.ts"; 2 | export { deserialize } from "./deserialize.ts"; 3 | export type { FieldSet, FieldType, JSON, ProtoBufEntry } from "./types.ts"; 4 | 5 | export { int32Field } from "./int32.ts"; 6 | export { int64Field } from "./int64.ts"; 7 | export { uint32Field } from "./uint32.ts"; 8 | export { uint64Field } from "./uint64.ts"; 9 | export { sint32Field } from "./sint32.ts"; 10 | export { sint64Field } from "./sint64.ts"; 11 | export { boolField } from "./bool.ts"; 12 | export { fixed64Field } from "./fixed64.ts"; 13 | export { sfixed64Field } from "./sfixed64.ts"; 14 | export { doubleField } from "./double.ts"; 15 | export { stringField } from "./string.ts"; 16 | export { bytesField } from "./bytes.ts"; 17 | export { fixed32Field } from "./fixed32.ts"; 18 | export { sfixed32Field } from "./sfixed32.ts"; 19 | export { floatField } from "./float.ts"; 20 | 21 | export { enumField } from "./enum.ts"; 22 | export { repeatedField } from "./repeated.ts"; 23 | export { packedField } from "./packed.ts"; 24 | export { mapField } from "./map.ts"; 25 | 26 | export { fromJSON } from "./from_json.ts"; 27 | export { fromBytes } from "./from_bytes.ts"; 28 | export { toJSON } from "./to_json.ts"; 29 | export { toBytes } from "./to_bytes.ts"; 30 | -------------------------------------------------------------------------------- /serialize.ts: -------------------------------------------------------------------------------- 1 | import { encode } from "./deps.ts"; 2 | import { concat } from "./concat.ts"; 3 | import type { ProtoBufEntry } from "./types.ts"; 4 | 5 | /** 6 | * Given `values` - an array of `ProtoBufentry` 3-tuples - encode each entry 7 | * into a ProtoBuf encoded Uint8Array. 8 | * 9 | * Importantly: this _does not_ encode signed numbers, using "ZigZag" encoding. 10 | * The protobuf wire types do not disambiguate which values need "ZigZag" 11 | * decoding so this must be done manually. 12 | */ 13 | export function serialize(values: Iterable): Uint8Array { 14 | let bytes = new Uint8Array(0); 15 | for (const value of values) { 16 | bytes = concat(bytes, encode((value[0] << 3) | value[1])[0]); 17 | switch (value[1]) { 18 | case 0: 19 | bytes = concat(bytes, encode(value[2])[0]); 20 | break; 21 | case 1: 22 | bytes = concat(bytes, value[2].slice(0, 8)); 23 | break; 24 | case 2: 25 | bytes = concat(bytes, encode(value[2].byteLength)[0]); 26 | bytes = concat(bytes, value[2]); 27 | break; 28 | case 5: 29 | bytes = concat(bytes, value[2].slice(0, 4)); 30 | break; 31 | default: 32 | throw new RangeError(`malformed wire type`); 33 | } 34 | } 35 | return bytes; 36 | } 37 | -------------------------------------------------------------------------------- /string_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertThrows, 4 | } from "https://deno.land/std@0.66.0/testing/asserts.ts"; 5 | import { stringField } from "./string.ts"; 6 | import { JSON } from "./types.ts"; 7 | 8 | Deno.test("stringField.fromBytes", () => { 9 | const tt: [Uint8Array, string][] = [ 10 | [Uint8Array.of(0x66, 0x6f, 0x6f), "foo"], 11 | ]; 12 | for (const [actual, expected] of tt) { 13 | assertEquals(stringField.fromBytes(actual), expected); 14 | } 15 | }); 16 | 17 | Deno.test("stringField.toBytes", () => { 18 | const tt: [string, Uint8Array][] = [ 19 | ["foo", Uint8Array.of(0x66, 0x6f, 0x6f)], 20 | ]; 21 | for (const [actual, expected] of tt) { 22 | assertEquals(stringField.toBytes(actual), expected); 23 | } 24 | }); 25 | 26 | Deno.test("stringField.fromJSON", () => { 27 | const tt: [string, string][] = [ 28 | ["foo", "foo"], 29 | ]; 30 | for (const [actual, expected] of tt) { 31 | assertEquals(stringField.fromJSON(actual), expected); 32 | } 33 | }); 34 | 35 | Deno.test("stringField.fromJSON failure", () => { 36 | const tt: NonNullable[] = [ 37 | 1, 38 | [], 39 | ]; 40 | for (const actual of tt) { 41 | assertThrows( 42 | () => stringField.fromJSON(actual), 43 | TypeError, 44 | "malformed json", 45 | ); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /from_bytes.ts: -------------------------------------------------------------------------------- 1 | import type { FieldSet, ProtoBufEntry } from "./types.ts"; 2 | import { deserialize } from "./deserialize.ts"; 3 | 4 | /** 5 | * Given a Message class, and a set of fields to extract with their respective 6 | * helper functions, this will serialize the Message into the ProtoBuf binary 7 | * format. 8 | */ 9 | export function fromBytes(bytes: Uint8Array, set: FieldSet): Partial { 10 | const fields = new Map(); 11 | for (const entry of deserialize(bytes)) { 12 | if (!fields.has(entry[0])) fields.set(entry[0], []); 13 | fields.get(entry[0])!.push(entry); 14 | } 15 | const init: Partial = {}; 16 | for (const key in set) { 17 | const [id, field] = set[key]!; 18 | if (fields.has(id)) { 19 | const entries = fields.get(id)!; 20 | if ("wireType" in field && "fromEntry" in field) { 21 | init[key] = field.fromEntry(entries); 22 | } else if ("wireType" in field) { 23 | const entry = entries.pop()!; 24 | if (entry[1] === 0 && field.wireType === 0) { 25 | init[key] = field.fromBytes(entry[2]); 26 | } else if (entry[1] !== 0 && entry[1] === field.wireType) { 27 | init[key] = field.fromBytes(entry[2]); 28 | } 29 | } else if ("fromBytes" in field) { 30 | const entry = entries.pop()!; 31 | if (entry[1] !== 0) init[key] = field.fromBytes(entry[2]); 32 | } 33 | } 34 | } 35 | return init; 36 | } 37 | -------------------------------------------------------------------------------- /deserialize.ts: -------------------------------------------------------------------------------- 1 | import { decode } from "./deps.ts"; 2 | import type { ProtoBufEntry } from "./types.ts"; 3 | 4 | /** 5 | * Given `bytes` (a ProtoBuf encoded binary payload), decode each block of 6 | * bytes as a `ProtoBufentry`. Each `ProtoBufentry` represents a field in the 7 | * protobuf binary payload. Entries have 3 constituent parts; the ID which 8 | * identifies the field number, the "wire type" which is a number between 0-5 9 | * and the "value" which will be a bigint for wireType 0, otherwise it will be 10 | * a Uint8Array 11 | * 12 | * Importantly: this _does not_ decode signed numbers, using "ZigZag" encoding. 13 | * The protobuf wire types do not disambiguate which values need "ZigZag" 14 | * decoding so this must be done manually. 15 | */ 16 | export function* deserialize(bytes: Uint8Array): Generator { 17 | for (let i = 0, v = 0n; i < bytes.length;) { 18 | [v, i] = decode(bytes, i); 19 | const id = Number(v >> 3n); 20 | switch (v & 7n) { 21 | case 0n: 22 | [v, i] = decode(bytes, i); 23 | yield [id, 0, v]; 24 | break; 25 | case 1n: 26 | yield [id, 1, bytes.slice(i, i += 8)]; 27 | break; 28 | case 2n: 29 | [v, i] = decode(bytes, i); 30 | yield [id, 2, bytes.slice(i, i += Number(v))]; 31 | break; 32 | case 5n: 33 | yield [id, 5, bytes.slice(i, i += 4)]; 34 | break; 35 | default: 36 | throw new RangeError(`malformed wire type ${Number(v & 7n)}`); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /map_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.66.0/testing/asserts.ts"; 2 | import { mapField } from "./map.ts"; 3 | import { int32Field } from "./int32.ts"; 4 | import { stringField } from "./string.ts"; 5 | import { FieldType, Message, ProtoBufEntry } from "./types.ts"; 6 | 7 | Deno.test("mapField.fromEntry", () => { 8 | const tt: [ 9 | ProtoBufEntry, 10 | Exclude, Message>, 11 | FieldType, 12 | Map, 13 | ][] = [ 14 | [ 15 | [ 16 | 1, 17 | 2, 18 | Uint8Array.of( 19 | ...[0x08, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x01], 20 | ...[0x10, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x01], 21 | ), 22 | ], 23 | int32Field, 24 | int32Field, 25 | new Map([[-2147483647, -2147483647]]), 26 | ], 27 | [ 28 | [ 29 | 1, 30 | 2, 31 | Uint8Array.of( 32 | ...[0x08, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x01], 33 | ...[0x12, 0x3, 0x66, 0x6f, 0x6f], 34 | ), 35 | ], 36 | int32Field, 37 | stringField, 38 | new Map([[-2147483647, "foo"]]), 39 | ], 40 | [ 41 | [ 42 | 1, 43 | 2, 44 | Uint8Array.of( 45 | ...[0x0a, 0x3, 0x66, 0x6f, 0x6f], 46 | ...[0x10, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x01], 47 | ), 48 | ], 49 | stringField, 50 | int32Field, 51 | new Map([["foo", -2147483647]]), 52 | ], 53 | ]; 54 | for (const [actual, key, value, expected] of tt) { 55 | assertEquals(mapField(key, value).fromEntry([actual]), expected); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /protod.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "https://deno.land/std@0.133.0/flags/mod.ts"; 2 | import { generate } from "./generate.ts"; 3 | import { version as v } from "./version.ts"; 4 | 5 | const { version, help, _: args } = parse(Deno.args, { 6 | string: [], 7 | boolean: ["help", "version"], 8 | stopEarly: true, 9 | alias: { 10 | "h": "help", 11 | "V": "version", 12 | }, 13 | }); 14 | 15 | const command = (args.shift() || "") as keyof typeof commands; 16 | 17 | const commands = { 18 | async gen(opts: (string | number)[]) { 19 | const { outfile, _: args } = parse(opts.map(String), { 20 | string: ["outfile"], 21 | boolean: [], 22 | stopEarly: true, 23 | alias: { 24 | "o": "outfile", 25 | }, 26 | }); 27 | const infile = String(args.shift() || ""); 28 | if (args.length) { 29 | console.error( 30 | `error: found argument ${command} which wasn't expected, or isn't valid in this context.`, 31 | ); 32 | Deno.exit(1); 33 | } 34 | if (!infile) { 35 | console.error(`error: missing argument. Must specify an input file.`); 36 | Deno.exit(1); 37 | } 38 | console.error(`Reading ${infile}`); 39 | const contents = await generate(infile); 40 | if (outfile) { 41 | Deno.writeTextFile(outfile, contents); 42 | } else { 43 | console.log(contents); 44 | } 45 | }, 46 | }; 47 | 48 | if (help) { 49 | console.error(` 50 | USAGE: 51 | protod [SUBCOMMAND] 52 | 53 | SUBCOMMANDS: 54 | gen Generate Deno code from a \`.proto\` file. 55 | `); 56 | Deno.exit(1); 57 | } else if (version) { 58 | console.error(`protod v${v}`); 59 | Deno.exit(1); 60 | } else if (!(command in commands)) { 61 | console.error( 62 | `error: found argument ${command} which wasn't expected, or isn't valid in this context`, 63 | ); 64 | Deno.exit(1); 65 | } else { 66 | await commands[command](args); 67 | } 68 | -------------------------------------------------------------------------------- /testdata/rpc1.pb.ts: -------------------------------------------------------------------------------- 1 | // Generated by protod v0.1.3 2 | /** 3 | * This is a generic file comment 4 | */ 5 | 6 | import { 7 | FieldSet, 8 | fromBytes, 9 | fromJSON, 10 | int32Field, 11 | JSON, 12 | stringField, 13 | toBytes, 14 | toJSON, 15 | } from "../mod.ts"; 16 | 17 | /** 18 | * This is a comment for MyRequest 19 | */ 20 | export class MyRequest { 21 | /** 22 | * This is a comment for path 23 | */ 24 | path: string; 25 | 26 | constructor(init: Partial) { 27 | this.path = init.path ?? ""; 28 | } 29 | 30 | static fields: FieldSet = { 31 | path: [1, stringField], 32 | }; 33 | 34 | static fromBytes(bytes: Uint8Array): MyRequest { 35 | return new MyRequest( 36 | fromBytes(bytes, MyRequest.fields), 37 | ); 38 | } 39 | 40 | static fromJSON(json: JSON): MyRequest { 41 | return new MyRequest( 42 | fromJSON(json, MyRequest.fields), 43 | ); 44 | } 45 | 46 | toBytes(): Uint8Array { 47 | return toBytes(this, MyRequest.fields); 48 | } 49 | 50 | toJSON() { 51 | return toJSON(this, MyRequest.fields); 52 | } 53 | } 54 | 55 | /** 56 | * This is a comment for MyResponse 57 | */ 58 | export class MyResponse { 59 | /** 60 | * This is a comment for status 61 | */ 62 | status: number; 63 | 64 | constructor(init: Partial) { 65 | this.status = init.status ?? 0; 66 | } 67 | 68 | static fields: FieldSet = { 69 | status: [2, int32Field], 70 | }; 71 | 72 | static fromBytes(bytes: Uint8Array): MyResponse { 73 | return new MyResponse( 74 | fromBytes(bytes, MyResponse.fields), 75 | ); 76 | } 77 | 78 | static fromJSON(json: JSON): MyResponse { 79 | return new MyResponse( 80 | fromJSON(json, MyResponse.fields), 81 | ); 82 | } 83 | 84 | toBytes(): Uint8Array { 85 | return toBytes(this, MyResponse.fields); 86 | } 87 | 88 | toJSON() { 89 | return toJSON(this, MyResponse.fields); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /repeated.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldType, 3 | JSON, 4 | Message, 5 | MessageInstance, 6 | MetaFieldBuf, 7 | ProtoBufEntry, 8 | } from "./types.ts"; 9 | 10 | /** 11 | * A helper object for Fields that are "Repeated" (not "Packed"). 12 | */ 13 | export const repeatedField = (of: FieldType): MetaFieldBuf => ({ 14 | name: "repeated", 15 | wireType: 2, 16 | 17 | fromBytes(): never { 18 | throw new Error("repeated fields must not use fromBytes"); 19 | }, 20 | 21 | toBytes(): never { 22 | throw new Error("repeated fields must not use toBytes"); 23 | }, 24 | 25 | fromJSON(value: NonNullable): T[] { 26 | if (Array.isArray(value)) { 27 | const newValue: T[] = []; 28 | for (const item of Array.from(value)) { 29 | if (item == null) { 30 | throw new Error(`malformed json`); 31 | } 32 | newValue.push(of.fromJSON(item)); 33 | } 34 | return newValue; 35 | } else { 36 | return [of.fromJSON(value)]; 37 | } 38 | }, 39 | toJSON(value: T[]): NonNullable { 40 | return value.map((value) => { 41 | if ("toJSON" in of) return of.toJSON(value); 42 | return (value as unknown as MessageInstance).toJSON(); 43 | }); 44 | }, 45 | 46 | fromEntry(entries: ProtoBufEntry[]): T[] { 47 | const ret: T[] = []; 48 | for (const entry of entries) { 49 | if ("wireType" in of && entry[1] === of.wireType) { 50 | if (entry[1] === 0 && of.wireType === 0) { 51 | ret.push(of.fromBytes(entry[2])); 52 | } else if (entry[1] !== 0 && of.wireType !== 0) { 53 | ret.push(of.fromBytes(entry[2])); 54 | } 55 | } else if (entry[1] !== 0) { 56 | ret.push((of as Message).fromBytes(entry[2])); 57 | } 58 | } 59 | return ret; 60 | }, 61 | 62 | toEntry(id: number, value: T[]): ProtoBufEntry[] { 63 | return value.map((val) => { 64 | if ("wireType" in of) { 65 | return [id, of.wireType, of.toBytes(val)] as ProtoBufEntry; 66 | } 67 | return [id, 2, (val as unknown as MessageInstance).toBytes()]; 68 | }); 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /packed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldType, 3 | JSON, 4 | Message, 5 | MetaFieldBuf, 6 | ProtoBufEntry, 7 | } from "./types.ts"; 8 | import { concat } from "./concat.ts"; 9 | import { decode, encode } from "./deps.ts"; 10 | 11 | /** 12 | * A helper object for Fields that need to use "Packed" encoding. 13 | */ 14 | export const packedField = ( 15 | of: Exclude, Message>, 16 | ): MetaFieldBuf => ({ 17 | name: "packed", 18 | wireType: 2, 19 | 20 | toBytes(): never { 21 | throw new Error("packed fields must use toEntry"); 22 | }, 23 | 24 | fromBytes(): never { 25 | throw new Error("packed fields must use fromEntry"); 26 | }, 27 | 28 | fromJSON(value: NonNullable): T[] { 29 | if (Array.isArray(value)) { 30 | const newValue: T[] = []; 31 | for (const item of Array.from(value)) { 32 | if (item == null) { 33 | throw new Error(`malformed json`); 34 | } 35 | newValue.push(of.fromJSON(item)); 36 | } 37 | return newValue; 38 | } else { 39 | return [of.fromJSON(value)]; 40 | } 41 | }, 42 | 43 | toJSON(value: T[]): JSON[] { 44 | return value.map((value) => of.toJSON(value)); 45 | }, 46 | 47 | fromEntry(entries: ProtoBufEntry[]): T[] { 48 | const values: T[] = []; 49 | for (const entry of entries) { 50 | if (entry[1] !== 2) continue; 51 | const bytes = entry[2]; 52 | for (let i = 0, v = 0n; i < bytes.byteLength; i) { 53 | let value: T; 54 | if (of.wireType === 1) { 55 | value = of.fromBytes(bytes.slice(i, i += 8)); 56 | } else if (of.wireType === 2) { 57 | [v, i] = decode(bytes, i); 58 | value = of.fromBytes(bytes.slice(i, i += Number(v))); 59 | } else if (of.wireType === 5) { 60 | value = of.fromBytes(bytes.slice(i, i += 4)); 61 | } else { 62 | [v, i] = decode(bytes, i); 63 | value = of.fromBytes(v); 64 | } 65 | values.push(value); 66 | } 67 | } 68 | return values; 69 | }, 70 | 71 | toEntry(id: number, value: T[]): ProtoBufEntry[] { 72 | let buf = new Uint8Array(0); 73 | for (const item of value) { 74 | const val = of.toBytes(item); 75 | if (val instanceof Uint8Array) { 76 | buf = concat(buf, val); 77 | } else { 78 | buf = concat(buf, encode(val)[0]); 79 | } 80 | } 81 | return [[id, 2, buf]]; 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protod 2 | 3 | This is a tool for consuming 4 | [Protocol Buffer DSL files](https://developers.google.com/protocol-buffers/docs/proto3) 5 | and extracting Messages and Enums out, converting them into TypeScript native 6 | code. 7 | 8 | This is somewhat similar to the 9 | [official `protoc` binary](https://github.com/protocolbuffers/protobuf), in that 10 | it can be used to generate code from the DSL, but this tool is very different in 11 | that it is fully written in TypeScript, and is designed to only output 12 | TypeScript. 13 | 14 | To install: 15 | 16 | ```sh 17 | # Needs `--allow-read` to be able to read .proto files 18 | # Needs `--allow-write` to be able to write pb.ts files 19 | deno install --allow-read --allow-write https://deno.land/x/protod/protod.ts 20 | 21 | protod gen my.proto 22 | ``` 23 | 24 | ### Potential Questions Asked 25 | 26 | #### What does the output look like? 27 | 28 | You can compare the example `.proto` and `.pb.ts` files in the 29 | [`./testdata/` directory](./testdata). They are designed to look like files a 30 | human might actually write; readability is important for debugability! 31 | 32 | #### Why isn't this a plugin for protoc? 33 | 34 | That would have potentially been much easier than writing an entire Protocol 35 | Buffers DSL parser from scratch, but there are several reasons why this tool was 36 | created: 37 | 38 | - The `protoc` binary is seriously unwieldy to use. 39 | - The Protocol Buffer "standard" is a single C library which is a manually 40 | written tokenizer/parser, and could really do with some alternative 41 | implementations. 42 | - Being fully written in TypeScript (the parser and generator) makes it easier 43 | for developers to get involved in the tooling. 44 | - The plan for this tool is to grow into linting, formatting, and doc 45 | generation, in the spirit of Deno/Go. 46 | 47 | Having said that, this tool - not being officially supported - should be 48 | considered caveat emptor. There are likely bugs or inconsistencies between this 49 | and official implementations. If you find one, please send a PR with a failing 50 | test! 51 | 52 | #### Why not just use the JS protoc plugin? 53 | 54 | Well, aside from answering some of that in the above question, there are also 55 | some problems with the official JS plugin. Chiefly: 56 | 57 | - It's written JS first, and while it does have TypeScript definitions, which 58 | means for example Enums are not TypeScript first class Enums. 59 | - The output is also relatively difficult to read - given it uses non-standard 60 | JS features such as the Google Closure Compiler module system. 61 | - The API the generated code features is a little unwieldy too. Method names 62 | like `setName()`, `setAge()` are used rather than simple field properties. 63 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A type representing the input/output types from serialize/deserialize. 3 | */ 4 | export type ProtoBufEntry = 5 | | [number, 0, bigint] 6 | | [number, 1, Uint8Array] 7 | | [number, 2, Uint8Array] 8 | | [number, 5, Uint8Array]; 9 | 10 | /** 11 | * A type to more accurately represent JSON values. 12 | * 13 | * (TypeScript defaults `JSON.parse/stringify` to be `any`). 14 | */ 15 | export type JSON = 16 | | { [prop: string]: JSON } 17 | | JSON[] 18 | | boolean 19 | | number 20 | | bigint 21 | | string 22 | | null 23 | | undefined; 24 | 25 | export type Field = 26 | | "string" 27 | | "int32"; 28 | 29 | export interface FieldTypeVarint { 30 | name: string; 31 | wireType: 0; 32 | fromBytes(value: bigint): T; 33 | fromJSON(value: NonNullable): T; 34 | toJSON(value: T): NonNullable; 35 | toBytes(value: T): bigint; 36 | } 37 | 38 | export interface FieldType64Bit { 39 | name: string; 40 | wireType: 1; 41 | fromBytes(value: Uint8Array): T; 42 | fromJSON(value: NonNullable): T; 43 | toJSON(value: T): NonNullable; 44 | toBytes(value: T): Uint8Array; 45 | } 46 | 47 | export interface FieldTypeLengthDelimited { 48 | name: string; 49 | wireType: 2; 50 | fromBytes(value: Uint8Array): T; 51 | fromJSON(value: NonNullable): T; 52 | toJSON(value: T): NonNullable; 53 | toBytes(value: T): Uint8Array; 54 | } 55 | 56 | export interface FieldType32Bit { 57 | name: string; 58 | wireType: 5; 59 | fromBytes(value: Uint8Array): T; 60 | fromJSON(value: NonNullable): T; 61 | toJSON(value: T): NonNullable; 62 | toBytes(value: T): Uint8Array; 63 | } 64 | 65 | export interface MetaFieldVarInt { 66 | name: string; 67 | wireType: 0; 68 | fromBytes(value: bigint): T; 69 | fromJSON(value: NonNullable): T; 70 | toJSON(value: T): NonNullable; 71 | toBytes(): never; 72 | fromEntry(entries: ProtoBufEntry[]): T; 73 | toEntry(id: number, value: T): ProtoBufEntry[]; 74 | } 75 | 76 | export interface MetaFieldBuf { 77 | name: string; 78 | wireType: 2; 79 | fromBytes(value: Uint8Array): T; 80 | fromJSON(value: NonNullable): T; 81 | toJSON(value: T): NonNullable; 82 | toBytes(): never; 83 | fromEntry(entries: ProtoBufEntry[]): T; 84 | toEntry(id: number, value: T): ProtoBufEntry[]; 85 | } 86 | 87 | /** 88 | * FieldType is a base type representing the possible Helper Objects that can 89 | * be created to encode/decode Message fields. 90 | */ 91 | export type FieldType = 92 | | FieldTypeVarint 93 | | FieldType64Bit 94 | | FieldTypeLengthDelimited 95 | | FieldType32Bit 96 | | MetaFieldVarInt 97 | | MetaFieldBuf 98 | | Message; 99 | 100 | export interface Message { 101 | new (init: Partial): T & MessageInstance; 102 | fromJSON(json: JSON): T & MessageInstance; 103 | fromBytes(bytes: Uint8Array): T & MessageInstance; 104 | } 105 | 106 | export interface MessageInstance { 107 | toBytes(): Uint8Array; 108 | toJSON(): JSON; 109 | } 110 | 111 | export type FieldSet = { 112 | [P in keyof T]?: [number, FieldType]; 113 | }; 114 | -------------------------------------------------------------------------------- /map.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldType, 3 | JSON, 4 | Message, 5 | MessageInstance, 6 | MetaFieldBuf, 7 | ProtoBufEntry, 8 | } from "./types.ts"; 9 | import { deserialize } from "./deserialize.ts"; 10 | import { serialize } from "./serialize.ts"; 11 | 12 | /** 13 | * A helper object for ProtoBuf Map fields. 14 | */ 15 | export const mapField = ( 16 | keyFn: Exclude, Message>, 17 | valueFn: FieldType, 18 | ): MetaFieldBuf> => ({ 19 | name: "map", 20 | wireType: 2, 21 | 22 | toBytes(): never { 23 | throw new Error("map fields must use toEntry"); 24 | }, 25 | 26 | fromBytes(): never { 27 | throw new Error("map fields must use fromEntry"); 28 | }, 29 | 30 | fromJSON(obj: NonNullable): Map { 31 | const map = new Map(); 32 | for (const [key, value] of Object.entries(obj)) { 33 | map.set(keyFn.fromJSON(key), valueFn.fromJSON(value)); 34 | } 35 | return map; 36 | }, 37 | 38 | toJSON(value: Map): NonNullable { 39 | const out: NonNullable = {}; 40 | for (const entry of value.entries()) { 41 | let value; 42 | if ("toJSON" in valueFn) { 43 | value = valueFn.toJSON(entry[1]); 44 | } else if ("toJSON" in entry[1]) { 45 | value = (entry[1] as unknown as MessageInstance).toJSON(); 46 | } 47 | out[String(keyFn.toJSON(entry[0]))] = value; 48 | } 49 | return out; 50 | }, 51 | 52 | fromEntry(entries: ProtoBufEntry[]): Map { 53 | const map: Map = new Map(); 54 | for (const topEntry of entries) { 55 | if (topEntry[1] !== 2) continue; 56 | let key: K | null = null; 57 | let value: V | null = null; 58 | for (const entry of deserialize(topEntry[2])) { 59 | if (entry[0] === 1) { 60 | if (entry[1] === 0 && keyFn.wireType === 0) { 61 | key = keyFn.fromBytes(entry[2]); 62 | } else if (entry[1] !== 0 && keyFn.wireType !== 0) { 63 | key = keyFn.fromBytes(entry[2]); 64 | } 65 | } else if (entry[0] === 2) { 66 | if ("wireType" in valueFn && entry[1] === valueFn.wireType) { 67 | if (entry[1] === 0 && valueFn.wireType === 0) { 68 | value = valueFn.fromBytes(entry[2]); 69 | } else if (entry[1] !== 0 && valueFn.wireType !== 0) { 70 | value = valueFn.fromBytes(entry[2]); 71 | } 72 | } else if ("fromBytes" in valueFn && entry[1] !== 0) { 73 | value = (valueFn as Message).fromBytes(entry[2]); 74 | } 75 | } 76 | if (key != null && value != null) { 77 | map.set(key, value); 78 | } 79 | } 80 | } 81 | return map; 82 | }, 83 | 84 | toEntry(id: number, value: Map): ProtoBufEntry[] { 85 | const entries: ProtoBufEntry[] = []; 86 | for (const [k, v] of value) { 87 | let value: ProtoBufEntry; 88 | if ("wireType" in valueFn) { 89 | value = [2, valueFn.wireType, valueFn.toBytes(v)] as ProtoBufEntry; 90 | } else { 91 | value = [ 92 | 2, 93 | 2, 94 | (v as unknown as MessageInstance).toBytes(), 95 | ] as ProtoBufEntry; 96 | } 97 | entries.push([ 98 | id, 99 | 2, 100 | serialize([ 101 | [1, keyFn.wireType, keyFn.toBytes(k)] as ProtoBufEntry, 102 | value, 103 | ]), 104 | ]); 105 | } 106 | return entries; 107 | }, 108 | }); 109 | -------------------------------------------------------------------------------- /generate_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.66.0/testing/asserts.ts"; 2 | import { generate } from "./generate.ts"; 3 | import { deserialize } from "./deserialize.ts"; 4 | import { JSON } from "./types.ts"; 5 | import { 6 | bench, 7 | BenchmarkTimer, 8 | runBenchmarks, 9 | } from "https://deno.land/std@0.74.0/testing/bench.ts"; 10 | 11 | interface Proto { 12 | toBytes(): Uint8Array; 13 | toJSON(): NonNullable; 14 | } 15 | interface ProtoS { 16 | name: string; 17 | fromBytes(b: Uint8Array): Proto; 18 | fromJSON(j: NonNullable): Proto; 19 | } 20 | 21 | const pbs = new Map(); 22 | const protos = new Set(); 23 | const messageTests = new Set(); 24 | for await (const { isFile, name } of Deno.readDir("./testdata")) { 25 | if (!isFile) continue; 26 | if (name.endsWith(".proto")) { 27 | protos.add(name); 28 | } else if (name.endsWith(".msg.json")) { 29 | messageTests.add(name); 30 | } else if (name.endsWith(".pb.ts")) { 31 | pbs.set(name, await import(`./testdata/${name}`)); 32 | } 33 | } 34 | 35 | let testCount = 0; 36 | 37 | for (const proto of protos) { 38 | const base = proto.replace(/.proto$/, ""); 39 | testCount += 1; 40 | Deno.test(base, async () => { 41 | assertEquals( 42 | (await generate(`./testdata/${proto}`, { mod: "../mod.ts" })).trim() 43 | .split("\n").slice(1), 44 | (await Deno.readTextFile(`./testdata/${base}.pb.ts`)).trim().split("\n") 45 | .slice(1), 46 | ); 47 | }); 48 | } 49 | for (const messageTest of messageTests) { 50 | const [_, base, n, message] = 51 | messageTest.match(/^(\w+)-(\d+)-(\w+).msg.json$/) || []; 52 | let Class: ProtoS | null = null; 53 | const mod = pbs.get(`${base}.pb.ts`)!; 54 | for (const className in mod) { 55 | if (className.toLowerCase() === message) { 56 | Class = mod[className]; 57 | } 58 | } 59 | testCount += 1; 60 | Deno.test(`${base} ${message} ${n}`, async () => { 61 | const bin = await Deno.readFile( 62 | `./testdata/${base}-${n}-${Class!.name.toLowerCase()}.msg.bin`, 63 | ); 64 | const json = JSON.parse( 65 | await Deno.readTextFile( 66 | `./testdata/${base}-${n}-${Class!.name.toLowerCase()}.msg.json`, 67 | ), 68 | ); 69 | const reqB = Class!.fromBytes(bin); 70 | const reqJ = Class!.fromJSON(json); 71 | const reqBJson = reqB.toJSON(); 72 | const reqJJson = reqJ.toJSON(); 73 | assertEquals(reqJJson, json); 74 | assertEquals(reqBJson, reqJJson); 75 | assertEquals(JSON.parse(JSON.stringify(reqBJson)), reqBJson); 76 | assertEquals(JSON.parse(JSON.stringify(reqJJson)), reqJJson); 77 | assertEquals(reqBJson, json); 78 | assertEquals(reqJJson, json); 79 | assertEquals(reqB.toBytes(), reqJ.toBytes()); 80 | assertEquals([...deserialize(reqB.toBytes())], [...deserialize(bin)]); 81 | assertEquals([...deserialize(reqJ.toBytes())], [...deserialize(bin)]); 82 | }); 83 | if (!Class) continue; 84 | bench({ 85 | name: `${base}${Class!.name}Bytes`, 86 | runs: 1000, 87 | async func(b: BenchmarkTimer) { 88 | const bin = await Deno.readFile( 89 | `./testdata/${base}-${n}-${Class!.name.toLowerCase()}.msg.bin`, 90 | ); 91 | b.start(); 92 | Class!.fromBytes(bin); 93 | b.stop(); 94 | }, 95 | }); 96 | bench({ 97 | name: `${base}${Class!.name}JSON`, 98 | runs: 1000, 99 | async func(b: BenchmarkTimer) { 100 | const json = JSON.parse( 101 | await Deno.readTextFile( 102 | `./testdata/${base}-${n}-${Class!.name.toLowerCase()}.msg.json`, 103 | ), 104 | ); 105 | b.start(); 106 | Class!.fromJSON(json); 107 | b.stop(); 108 | }, 109 | }); 110 | } 111 | 112 | Deno.test("testCount", () => { 113 | assertEquals(testCount >= 9, true, `only saw ${testCount} tests`); 114 | }); 115 | 116 | await runBenchmarks({}); 117 | -------------------------------------------------------------------------------- /testdata/fields.pb.ts: -------------------------------------------------------------------------------- 1 | // Generated by protod v0.1.3 2 | import { 3 | boolField, 4 | bytesField, 5 | doubleField, 6 | enumField, 7 | FieldSet, 8 | fixed32Field, 9 | fixed64Field, 10 | fromBytes, 11 | fromJSON, 12 | int32Field, 13 | int64Field, 14 | JSON, 15 | mapField, 16 | packedField, 17 | repeatedField, 18 | sfixed32Field, 19 | sfixed64Field, 20 | sint32Field, 21 | sint64Field, 22 | stringField, 23 | toBytes, 24 | toJSON, 25 | uint32Field, 26 | uint64Field, 27 | } from "../mod.ts"; 28 | 29 | export enum Enum { 30 | a = 0, 31 | b = 1, 32 | c = 2, 33 | } 34 | 35 | export class Sub { 36 | a: string; 37 | 38 | constructor(init: Partial) { 39 | this.a = init.a ?? ""; 40 | } 41 | 42 | static fields: FieldSet = { 43 | a: [1, stringField], 44 | }; 45 | 46 | static fromBytes(bytes: Uint8Array): Sub { 47 | return new Sub( 48 | fromBytes(bytes, Sub.fields), 49 | ); 50 | } 51 | 52 | static fromJSON(json: JSON): Sub { 53 | return new Sub( 54 | fromJSON(json, Sub.fields), 55 | ); 56 | } 57 | 58 | toBytes(): Uint8Array { 59 | return toBytes(this, Sub.fields); 60 | } 61 | 62 | toJSON() { 63 | return toJSON(this, Sub.fields); 64 | } 65 | } 66 | 67 | export class Fields { 68 | a: number; 69 | b: bigint; 70 | c: number; 71 | d: bigint; 72 | e: number; 73 | f: bigint; 74 | g: boolean; 75 | h: Enum; 76 | i: bigint; 77 | j: bigint; 78 | k: number; 79 | l: string; 80 | m: Uint8Array; 81 | n: Sub; 82 | o: number; 83 | p: number; 84 | q: Map; 85 | r: Map; 86 | s: Map; 87 | t: Map; 88 | u: bigint[]; 89 | v: boolean[]; 90 | w: string[]; 91 | x: Sub[]; 92 | #y: string | void = undefined; 93 | get y(): string | void { 94 | return this.#y; 95 | } 96 | set y(value: string | void) { 97 | this.#y = value || ""; 98 | this.#z = undefined; 99 | } 100 | #z: number | void = undefined; 101 | get z(): number | void { 102 | return this.#z; 103 | } 104 | set z(value: number | void) { 105 | this.#y = undefined; 106 | this.#z = value || 0; 107 | } 108 | 109 | constructor(init: Partial) { 110 | this.a = init.a ?? 0; 111 | this.b = init.b ?? 0n; 112 | this.c = init.c ?? 0; 113 | this.d = init.d ?? 0n; 114 | this.e = init.e ?? 0; 115 | this.f = init.f ?? 0n; 116 | this.g = init.g ?? false; 117 | this.h = init.h ?? Enum.a; 118 | this.i = init.i ?? 0n; 119 | this.j = init.j ?? 0n; 120 | this.k = init.k ?? 0; 121 | this.l = init.l ?? ""; 122 | this.m = init.m ?? new Uint8Array(0); 123 | this.n = init.n ?? new Sub({}); 124 | this.o = init.o ?? 0; 125 | this.p = init.p ?? 0; 126 | this.q = init.q ?? new Map(); 127 | this.r = init.r ?? new Map(); 128 | this.s = init.s ?? new Map(); 129 | this.t = init.t ?? new Map(); 130 | this.u = init.u ?? []; 131 | this.v = init.v ?? []; 132 | this.w = init.w ?? []; 133 | this.x = init.x ?? []; 134 | if ("y" in init) { 135 | this.y = init.y ?? undefined; 136 | } else if ("z" in init) { 137 | this.z = init.z ?? undefined; 138 | } 139 | } 140 | 141 | static fields: FieldSet = { 142 | a: [1, int32Field], 143 | b: [2, int64Field], 144 | c: [3, uint32Field], 145 | d: [4, uint64Field], 146 | e: [5, sint32Field], 147 | f: [6, sint64Field], 148 | g: [7, boolField], 149 | h: [8, enumField(Enum)], 150 | i: [9, fixed64Field], 151 | j: [10, sfixed64Field], 152 | k: [11, doubleField], 153 | l: [12, stringField], 154 | m: [13, bytesField], 155 | n: [14, Sub], 156 | o: [15, fixed32Field], 157 | p: [16, sfixed32Field], 158 | q: [17, mapField(int32Field, int32Field)], 159 | r: [18, mapField(int32Field, stringField)], 160 | s: [19, mapField(stringField, int32Field)], 161 | t: [20, mapField(uint64Field, enumField(Enum))], 162 | u: [21, packedField(uint64Field)], 163 | v: [22, packedField(boolField)], 164 | w: [23, repeatedField(stringField)], 165 | x: [24, repeatedField(Sub)], 166 | y: [25, stringField], 167 | z: [26, int32Field], 168 | }; 169 | 170 | static fromBytes(bytes: Uint8Array): Fields { 171 | return new Fields( 172 | fromBytes(bytes, Fields.fields), 173 | ); 174 | } 175 | 176 | static fromJSON(json: JSON): Fields { 177 | return new Fields( 178 | fromJSON(json, Fields.fields), 179 | ); 180 | } 181 | 182 | toBytes(): Uint8Array { 183 | return toBytes(this, Fields.fields); 184 | } 185 | 186 | toJSON() { 187 | return toJSON(this, Fields.fields); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /serialize_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.66.0/testing/asserts.ts"; 2 | import { deserialize, ProtoBufEntry, serialize } from "./mod.ts"; 3 | 4 | function assertMod(bytes: Uint8Array, value: ProtoBufEntry[]) { 5 | let i = -1; 6 | for (const entry of deserialize(bytes)) assertEquals(entry, value[i += 1]); 7 | assertEquals(Array.from(deserialize(bytes)), value); 8 | assertEquals(Array.from(deserialize(serialize(value))), value); 9 | assertEquals(serialize(deserialize(bytes)), bytes); 10 | } 11 | 12 | Deno.test("Serialize wireType 0", () => { 13 | assertMod(Uint8Array.of(0x10, 0x01), [[2, 0, 1n]]); 14 | assertMod(Uint8Array.of(0x08, 0x96, 0x01), [[1, 0, 150n]]); 15 | assertMod( 16 | Uint8Array.of(0x10, 0x01, 0x08, 0x96, 0x01), 17 | [[2, 0, 1n], [1, 0, 150n]], 18 | ); 19 | }); 20 | 21 | Deno.test("Serialize wireType 2", () => { 22 | assertMod( 23 | Uint8Array.of(0x0a, 0x03, 0x66, 0x6f, 0x6f), 24 | [[1, 2, Uint8Array.of(0x66, 0x6f, 0x6f)]], 25 | ); 26 | assertMod( 27 | Uint8Array.of(0x12, 0x07, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67), 28 | [[2, 2, Uint8Array.of(0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67)]], 29 | ); 30 | assertMod( 31 | Uint8Array.of(0x1a, 0x03, 0x08, 0x96, 0x01), 32 | [[3, 2, Uint8Array.of(0x08, 0x96, 0x01)]], 33 | ); 34 | assertMod( 35 | Uint8Array.of( 36 | ...[0x0a, 0x03, 0x66, 0x6f, 0x6f, 0x12, 0x07, 0x74], 37 | ...[0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x1a, 0x03], 38 | ...[0x08, 0x96, 0x01], 39 | ), 40 | [ 41 | [1, 2, Uint8Array.of(0x66, 0x6f, 0x6f)], 42 | [2, 2, Uint8Array.of(0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67)], 43 | [3, 2, Uint8Array.of(0x08, 0x96, 0x01)], 44 | ], 45 | ); 46 | }); 47 | 48 | Deno.test("Serialize Fields", () => { 49 | assertMod( 50 | Uint8Array.of( 51 | ...[0x08, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff, 0xff], 52 | ...[0xff, 0xff, 0x01, 0x10, 0x80, 0x80, 0x80, 0x80], 53 | ...[0x80, 0x80, 0x80, 0x80, 0x80, 0x01, 0x18, 0xff], 54 | ...[0xff, 0xff, 0xff, 0x0f, 0x20, 0xff, 0xff, 0xff], 55 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x28], 56 | ...[0xfd, 0xff, 0xff, 0xff, 0x0f, 0x30, 0xff, 0xff], 57 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], 58 | ...[0x38, 0x01, 0x40, 0x01, 0x49, 0xff, 0xff, 0xff], 59 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0x51, 0x00, 0x00], 60 | ...[0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x59, 0xff], 61 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x7f, 0x62], 62 | ...[0x03, 0x66, 0x6f, 0x6f, 0x6a, 0x06, 0x66, 0x66], 63 | ...[0x66, 0x66, 0x66, 0x66, 0x72, 0x05, 0x0a, 0x03], 64 | ...[0x66, 0x6f, 0x6f, 0x7d, 0xff, 0xff, 0xff, 0xff], 65 | ...[0x85, 0x01, 0x01, 0x00, 0x00, 0x80, 0x8a, 0x01], 66 | ...[0x16, 0x08, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff], 67 | ...[0xff, 0xff, 0xff, 0x01, 0x10, 0x81, 0x80, 0x80], 68 | ...[0x80, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x01, 0x92], 69 | ...[0x01, 0x10, 0x08, 0x81, 0x80, 0x80, 0x80, 0xf8], 70 | ...[0xff, 0xff, 0xff, 0xff, 0x01, 0x12, 0x03, 0x66], 71 | ...[0x6f, 0x6f, 0x9a, 0x01, 0x10, 0x0a, 0x03, 0x66], 72 | ...[0x6f, 0x6f, 0x10, 0x81, 0x80, 0x80, 0x80, 0xf8], 73 | ...[0xff, 0xff, 0xff, 0xff, 0x01, 0xa2, 0x01, 0x0d], 74 | ...[0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 75 | ...[0xff, 0xff, 0x01, 0x10, 0x02, 0xaa, 0x01, 0x1e], 76 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 77 | ...[0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 78 | ...[0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0xff], 79 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xb2, 0x01], 80 | ...[0x03, 0x00, 0x01, 0x00, 0xba, 0x01, 0x03, 0x66], 81 | ...[0x6f, 0x6f, 0xba, 0x01, 0x03, 0x62, 0x61, 0x72], 82 | ...[0xba, 0x01, 0x03, 0x62, 0x61, 0x7a, 0xca, 0x01], 83 | ...[0x03, 0x66, 0x6f, 0x6f], 84 | ), 85 | [ 86 | [1, 0, 18446744071562067969n], 87 | [2, 0, 9223372036854775808n], 88 | [3, 0, 4294967295n], 89 | [4, 0, 18446744073709551615n], 90 | [5, 0, 4294967293n], 91 | [6, 0, 18446744073709551615n], 92 | [7, 0, 1n], 93 | [8, 0, 1n], 94 | [9, 1, Uint8Array.of(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff)], 95 | [10, 1, Uint8Array.of(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80)], 96 | [11, 1, Uint8Array.of(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x7f)], 97 | [12, 2, Uint8Array.of(0x66, 0x6f, 0x6f)], 98 | [13, 2, Uint8Array.of(0x66, 0x66, 0x66, 0x66, 0x66, 0x66)], 99 | [14, 2, Uint8Array.of(0x0a, 0x03, 0x66, 0x6f, 0x6f)], 100 | [15, 5, Uint8Array.of(0xff, 0xff, 0xff, 0xff)], 101 | [16, 5, Uint8Array.of(0x01, 0x00, 0x00, 0x80)], 102 | [ 103 | 17, 104 | 2, 105 | Uint8Array.of( 106 | ...[0x08, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff, 0xff], 107 | ...[0xff, 0xff, 0x01, 0x10, 0x81, 0x80, 0x80, 0x80], 108 | ...[0xf8, 0xff, 0xff, 0xff, 0xff, 0x01], 109 | ), 110 | ], 111 | [ 112 | 18, 113 | 2, 114 | Uint8Array.of( 115 | ...[0x08, 0x81, 0x80, 0x80, 0x80, 0xf8, 0xff, 0xff], 116 | ...[0xff, 0xff, 0x01, 0x12, 0x03, 0x66, 0x6f, 0x6f], 117 | ), 118 | ], 119 | [ 120 | 19, 121 | 2, 122 | Uint8Array.of( 123 | ...[0x0a, 0x03, 0x66, 0x6f, 0x6f, 0x10, 0x81, 0x80], 124 | ...[0x80, 0x80, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x01], 125 | ), 126 | ], 127 | [ 128 | 20, 129 | 2, 130 | Uint8Array.of( 131 | ...[0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 132 | ...[0xff, 0xff, 0x01, 0x10, 0x02], 133 | ), 134 | ], 135 | [ 136 | 21, 137 | 2, 138 | Uint8Array.of( 139 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 140 | ...[0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 141 | ...[0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0xff], 142 | ...[0xff, 0xff, 0xff, 0xff, 0xff, 0x01], 143 | ), 144 | ], 145 | [22, 2, Uint8Array.of(0x00, 0x01, 0x00)], 146 | [23, 2, Uint8Array.of(0x66, 0x6f, 0x6f)], 147 | [23, 2, Uint8Array.of(0x62, 0x61, 0x72)], 148 | [23, 2, Uint8Array.of(0x62, 0x61, 0x7a)], 149 | [25, 2, Uint8Array.of(0x66, 0x6f, 0x6f)], 150 | ], 151 | ); 152 | }); 153 | -------------------------------------------------------------------------------- /generate.ts: -------------------------------------------------------------------------------- 1 | import { version } from "./version.ts"; 2 | 3 | import { 4 | dirname, 5 | Enum, 6 | EnumField, 7 | Field, 8 | Import, 9 | join, 10 | MapField, 11 | Message, 12 | Oneof, 13 | Option, 14 | parse, 15 | Proto, 16 | Syntax, 17 | Visitor, 18 | } from "./deps.ts"; 19 | 20 | enum Type { 21 | int32 = "int32", 22 | int64 = "int64", 23 | uint32 = "uint32", 24 | uint64 = "uint64", 25 | sint32 = "sint32", 26 | sint64 = "sint64", 27 | bool = "bool", 28 | "enum" = "enum", 29 | fixed64 = "fixed64", 30 | sfixed64 = "sfixed64", 31 | double = "double", 32 | string = "string", 33 | bytes = "bytes", 34 | message = "message", 35 | fixed32 = "fixed32", 36 | sfixed32 = "sfixed32", 37 | float = "float", 38 | } 39 | 40 | const WireTypes: Record = { 41 | int32: 0, 42 | int64: 0, 43 | uint32: 0, 44 | uint64: 0, 45 | sint32: 0, 46 | sint64: 0, 47 | bool: 0, 48 | enum: 0, 49 | fixed64: 1, 50 | sfixed64: 1, 51 | double: 1, 52 | string: 2, 53 | bytes: 2, 54 | message: 2, 55 | fixed32: 5, 56 | sfixed32: 5, 57 | float: 5, 58 | }; 59 | 60 | function* getFields( 61 | message: Message | Oneof, 62 | ): Generator { 63 | for (const statement of message.body) { 64 | if ( 65 | statement instanceof Field || 66 | statement instanceof Oneof || 67 | statement instanceof MapField 68 | ) { 69 | yield statement; 70 | } 71 | } 72 | } 73 | 74 | function isPrimitive( 75 | field: Field, 76 | ): field is Field & { fieldType: { name: Type } } { 77 | return Type[field.fieldType.name as Type] === field.fieldType.name; 78 | } 79 | 80 | function isPackedField(field: Field, syntax: 2 | 3): boolean { 81 | if ( 82 | field.repeated && syntax === 3 && isPrimitive(field) && 83 | !WireTypes[field.fieldType.name] 84 | ) { 85 | return true; 86 | } 87 | let ret = false; 88 | field.accept({ 89 | visitOption(opt: Option) { 90 | if ( 91 | opt.key.length === 1 && opt.key[0] === "packed" && 92 | opt.value.value === true 93 | ) { 94 | ret = true; 95 | } 96 | }, 97 | }); 98 | return ret; 99 | } 100 | 101 | const TypeNativeMap: Record = { 102 | "int32": "number", 103 | "int64": "bigint", 104 | "uint32": "number", 105 | "uint64": "bigint", 106 | "sint32": "number", 107 | "sint64": "bigint", 108 | "bool": "boolean", 109 | "enum": "", 110 | "fixed64": "bigint", 111 | "sfixed64": "bigint", 112 | "double": "number", 113 | "string": "string", 114 | "bytes": "Uint8Array", 115 | "message": "", 116 | "fixed32": "number", 117 | "sfixed32": "number", 118 | "float": "number", 119 | }; 120 | 121 | function getFieldNativeType(field: Field | Oneof | MapField): string { 122 | if (field instanceof MapField) { 123 | const keyType = TypeNativeMap[field.keyType]; 124 | const valueType = TypeNativeMap[field.valueType.name as Type] || 125 | field.valueType.name; 126 | return `Map<${keyType}, ${valueType}>`; 127 | } 128 | if (field instanceof Oneof) { 129 | return [...getFields(field)] 130 | .map((field) => getFieldNativeType(field)) 131 | .join(" | "); 132 | } 133 | if (isPrimitive(field)) { 134 | return `${TypeNativeMap[field.fieldType.name]}${ 135 | field.repeated ? "[]" : "" 136 | }`; 137 | } 138 | return `${field.fieldType.name}${field.repeated ? "[]" : ""}`; 139 | } 140 | 141 | function hasScopedEnum(proto: ProtoGenerator, name: string): string | void { 142 | for (const [source, idents] of proto.scopedEnums) { 143 | if (idents.has(name)) return source; 144 | } 145 | } 146 | 147 | function hasScopedMessage(proto: ProtoGenerator, name: string): string | void { 148 | for (const [source, idents] of proto.scopedMessages) { 149 | if (idents.has(name)) return source; 150 | } 151 | } 152 | 153 | function getFieldTypeFn( 154 | proto: ProtoGenerator, 155 | field: Field | Oneof | MapField, 156 | ): [string, number] { 157 | if (field instanceof MapField) { 158 | proto.imports.from(proto.mod).import("mapField"); 159 | if (field.valueType.name in Type) { 160 | proto.imports.from(proto.mod).import(`${field.valueType.name}Field`); 161 | return [ 162 | `mapField(${field.keyType}Field, ${field.valueType.name}Field)`, 163 | 2, 164 | ]; 165 | } else if (proto.enums.has(field.valueType.name)) { 166 | proto.imports.from(proto.mod).import("enumField"); 167 | return [ 168 | `mapField(${field.keyType}Field, enumField(${field.valueType.name}))`, 169 | 2, 170 | ]; 171 | } 172 | } else if (field instanceof Oneof) { 173 | return ["", 0]; 174 | } else { 175 | let fieldType = ""; 176 | let wireType = 0; 177 | if (proto.enums.has(field.fieldType.name)) { 178 | proto.imports.from(proto.mod).import("enumField"); 179 | fieldType = `enumField(${field.fieldType.name})`; 180 | wireType = 0; 181 | } else if (proto.messages.has(field.fieldType.name)) { 182 | fieldType = `${field.fieldType.name}`; 183 | wireType = 2; 184 | } else if (field.fieldType.name in Type) { 185 | proto.imports.from(proto.mod).import(`${field.fieldType.name}Field`); 186 | fieldType = `${field.fieldType.name}Field`; 187 | wireType = WireTypes[field.fieldType.name as Type]; 188 | } else { 189 | let mod: string | void = hasScopedEnum(proto, field.fieldType.name); 190 | if (mod) { 191 | wireType = 1; 192 | proto.imports.from(proto.mod).import("enumField"); 193 | proto.imports.from(mod || "./deps.ts").import(field.fieldType.name); 194 | fieldType = `enumField(${field.fieldType.name})`; 195 | } else { 196 | mod = hasScopedMessage(proto, field.fieldType.name); 197 | if (mod) { 198 | wireType = 2; 199 | proto.imports.from(mod || "./deps.ts").import(field.fieldType.name); 200 | fieldType = field.fieldType.name; 201 | } 202 | } 203 | } 204 | if (field.repeated) { 205 | const isPacked = isPackedField(field, proto.syntax); 206 | const wrapper = isPacked ? "packedField" : "repeatedField"; 207 | proto.imports.from(proto.mod).import(wrapper); 208 | fieldType = `${wrapper}(${fieldType})`; 209 | wireType = isPacked ? 2 : WireTypes[field.fieldType.name as Type]; 210 | } 211 | return [fieldType, wireType]; 212 | } 213 | throw new Error(`unknown field ${field}`); 214 | } 215 | 216 | function getDefaultValue(field: Field | Oneof | MapField): string { 217 | if (field instanceof MapField) { 218 | return "new Map()"; 219 | } 220 | if (field instanceof Oneof) { 221 | return "null"; 222 | } 223 | const id = field.fieldType.name; 224 | if (field.repeated) { 225 | return "[]"; 226 | } 227 | switch (id) { 228 | case "int32": 229 | return "0"; 230 | case "int64": 231 | return "0n"; 232 | case "uint32": 233 | return "0"; 234 | case "uint64": 235 | return "0n"; 236 | case "sint32": 237 | return "0"; 238 | case "sint64": 239 | return "0n"; 240 | case "bool": 241 | return "false"; 242 | case "enum": 243 | return "0"; 244 | case "fixed64": 245 | return "0n"; 246 | case "sfixed64": 247 | return "0n"; 248 | case "double": 249 | return "0"; 250 | case "string": 251 | return '""'; 252 | case "bytes": 253 | return "new Uint8Array(0)"; 254 | case "message": 255 | return "{}"; 256 | case "fixed32": 257 | return "0"; 258 | case "sfixed32": 259 | return "0"; 260 | case "float": 261 | return "0"; 262 | default: 263 | return `new ${id}({})`; 264 | } 265 | } 266 | 267 | class EnumGenerator { 268 | default = ""; 269 | 270 | constructor(private enumField: Enum) { 271 | for (const field of this.enumField.body) { 272 | if (field instanceof EnumField && field.id === 0) { 273 | this.default = field.name; 274 | break; 275 | } 276 | } 277 | } 278 | 279 | *[Symbol.iterator](): Generator { 280 | yield `export enum ${this.enumField.name} {`; 281 | for (const field of this.enumField.body) { 282 | if (field instanceof EnumField) { 283 | yield ` ${field.name} = ${field.id},`; 284 | } 285 | } 286 | yield `}`; 287 | } 288 | 289 | toString() { 290 | return [...this].join("\n"); 291 | } 292 | } 293 | 294 | class ImportGenerator { 295 | #imports = new Set(); 296 | constructor(public module: string) {} 297 | 298 | import(...ids: string[]) { 299 | for (const id of ids) this.#imports.add(id); 300 | return this; 301 | } 302 | 303 | *[Symbol.iterator](): Generator { 304 | for ( 305 | const id of [...this.#imports].sort((a, b) => 306 | a.toLowerCase().localeCompare(b.toLowerCase()) 307 | ) 308 | ) { 309 | yield id; 310 | } 311 | } 312 | 313 | toString() { 314 | const line = `import { ` + [...this].join(", ") + 315 | ` } from "${this.module}";`; 316 | if (line.length >= 80) { 317 | return `import {\n ` + [...this].join(",\n ") + 318 | `,\n} from "${this.module}";`; 319 | } 320 | return line; 321 | } 322 | } 323 | 324 | class ImportMap { 325 | #imports = new Set(); 326 | 327 | from(id: string): ImportGenerator { 328 | for (const importGenerator of this.#imports) { 329 | if (importGenerator.module === id) return importGenerator; 330 | } 331 | const importGenerator = new ImportGenerator(id); 332 | this.#imports.add(importGenerator); 333 | return importGenerator; 334 | } 335 | 336 | *[Symbol.iterator]() { 337 | for (const importGenerator of this.#imports) { 338 | yield importGenerator.toString(); 339 | } 340 | } 341 | 342 | toString() { 343 | return [...this].join("\n"); 344 | } 345 | } 346 | 347 | class MessageGenerator { 348 | imports: ImportMap; 349 | 350 | constructor(private message: Message, private parent: ProtoGenerator) { 351 | this.imports = this.parent.imports; 352 | } 353 | 354 | private *comments(): Generator { 355 | for (const comment of this.message.comments) { 356 | yield comment.body; 357 | } 358 | } 359 | 360 | private *oneOfFieldGetter(field: Oneof): Generator { 361 | for (const subField of getFields(field)) { 362 | const type = getFieldNativeType(subField); 363 | yield `#${subField.name}: ${type} | void = undefined;`; 364 | yield `get ${subField.name}(): ${type} | void {`; 365 | yield ` return this.#${subField.name};`; 366 | yield `}`; 367 | yield `set ${subField.name}(value: ${type} | void) {`; 368 | for (const otherField of getFields(field)) { 369 | const value = getDefaultValue(otherField); 370 | const isSelf = otherField === subField; 371 | const assignment = isSelf ? `value || ${value}` : "undefined"; 372 | yield ` this.#${otherField.name} = ${assignment};`; 373 | } 374 | yield `}`; 375 | } 376 | } 377 | 378 | private *classGetters(): Generator { 379 | for (const field of getFields(this.message)) { 380 | if (field instanceof Oneof) { 381 | for (const line of this.oneOfFieldGetter(field)) { 382 | yield line; 383 | } 384 | } 385 | } 386 | } 387 | 388 | private *classFields(): Generator { 389 | for (const field of getFields(this.message)) { 390 | if (field instanceof Oneof) continue; 391 | const fieldNativeType = getFieldNativeType(field); 392 | for (const comment of (field as Field).comments || []) { 393 | for (const line of comment.body.split("\n")) yield line; 394 | } 395 | yield `${field.name}: ${fieldNativeType};`; 396 | } 397 | } 398 | 399 | private *classConstructor(): Generator { 400 | if (!this.message.body.length) { 401 | yield `constructor() {}`; 402 | return; 403 | } 404 | yield `constructor(init: Partial<${this.message.name}>) {`; 405 | for (const field of getFields(this.message)) { 406 | const name = field.name; 407 | if (field instanceof MapField) { 408 | yield ` this.${name} = init.${name} ?? new Map();`; 409 | } else if (field instanceof Oneof) { 410 | let i = 0; 411 | for (const subField of getFields(field)) { 412 | if (i === 0) { 413 | yield ` if ("${subField.name}" in init) {`; 414 | } else { 415 | yield ` } else if ("${subField.name}" in init) {`; 416 | } 417 | yield ` this.${subField.name} = init.${subField.name} ?? undefined;`; 418 | i += 1; 419 | } 420 | yield " }"; 421 | } else if (field.repeated) { 422 | yield ` this.${name} = init.${name} ?? [];`; 423 | } else if (this.parent.messages.has(field.fieldType.name)) { 424 | yield ` this.${name} = init.${name} ?? new ${field.fieldType.name}({});`; 425 | } else if (this.parent.enums.has(field.fieldType.name)) { 426 | const Enum = this.parent.enums.get(field.fieldType.name)!; 427 | yield ` this.${name} = init.${name} ?? ${field.fieldType.name}.${Enum.default};`; 428 | } else if (hasScopedEnum(this.parent, field.fieldType.name)) { 429 | yield ` this.${name} = init.${name} ?? 0;`; 430 | } else { 431 | const defaultValue = getDefaultValue(field); 432 | yield ` this.${name} = init.${name} ?? ${defaultValue};`; 433 | } 434 | } 435 | yield `}`; 436 | } 437 | 438 | private *fieldsField(): Generator { 439 | if (!this.message.body.length) { 440 | yield `static fields = {};`; 441 | return; 442 | } 443 | this.imports.from(this.parent.mod).import("FieldSet"); 444 | yield `static fields: FieldSet<${this.message.name}> = {`; 445 | for (const field of getFields(this.message)) { 446 | if (field instanceof Oneof) { 447 | for (const subField of getFields(field)) { 448 | yield ` ${subField.name}: [${(subField as Field).id}, ${ 449 | getFieldTypeFn(this.parent, subField)[0] 450 | }],`; 451 | } 452 | } else { 453 | yield ` ${field.name}: [${field.id}, ${ 454 | getFieldTypeFn(this.parent, field)[0] 455 | }],`; 456 | } 457 | } 458 | yield `};`; 459 | } 460 | 461 | private *fromBytesMethod(): Generator { 462 | if (!this.message.body.length) { 463 | yield `static fromBytes(): ${this.message.name} {`; 464 | yield ` return new ${this.message.name}();`; 465 | yield `}`; 466 | return; 467 | } 468 | yield `static fromBytes(bytes: Uint8Array): ${this.message.name} {`; 469 | this.imports.from(this.parent.mod).import("fromBytes"); 470 | yield ` return new ${this.message.name}(`; 471 | yield ` fromBytes<${this.message.name}>(bytes, ${this.message.name}.fields),`; 472 | yield ` );`; 473 | yield `}`; 474 | } 475 | 476 | private *fromJSONMethod() { 477 | if (!this.message.body.length) { 478 | yield `static fromJSON(): ${this.message.name} {`; 479 | yield ` return new ${this.message.name}();`; 480 | yield `}`; 481 | return; 482 | } 483 | this.imports.from(this.parent.mod).import("JSON", "fromJSON"); 484 | yield `static fromJSON(json: JSON): ${this.message.name} {`; 485 | yield ` return new ${this.message.name}(`; 486 | yield ` fromJSON<${this.message.name}>(json, ${this.message.name}.fields),`; 487 | yield ` );`; 488 | yield `}`; 489 | } 490 | 491 | private *toBytesMethod(): Generator { 492 | if (!this.message.body.length) { 493 | yield `toBytes(): Uint8Array {`; 494 | yield ` return Uint8Array.of();`; 495 | yield `}`; 496 | return; 497 | } 498 | this.imports.from(this.parent.mod).import("toBytes"); 499 | yield `toBytes(): Uint8Array {`; 500 | yield ` return toBytes<${this.message.name}>(this, ${this.message.name}.fields);`; 501 | yield `}`; 502 | } 503 | 504 | private *toJSONMethod(): Generator { 505 | if (!this.message.body.length) { 506 | yield `toJSON() {`; 507 | yield ` return {};`; 508 | yield `}`; 509 | return; 510 | } 511 | this.parent.imports.from(this.parent.mod).import("toJSON"); 512 | yield `toJSON() {`; 513 | yield ` return toJSON<${this.message.name}>(this, ${this.message.name}.fields);`; 514 | yield `}`; 515 | } 516 | 517 | *[Symbol.iterator](): Generator { 518 | for (const line of this.comments()) yield line; 519 | yield `export class ${this.message.name} {`; 520 | for (const line of this.classFields()) yield ` ${line}`; 521 | for (const line of this.classGetters()) yield ` ${line}`; 522 | if (this.message.body.length) yield ""; 523 | for (const line of this.classConstructor()) yield ` ${line}`; 524 | yield ""; 525 | for (const line of this.fieldsField()) yield ` ${line}`; 526 | yield ""; 527 | for (const line of this.fromBytesMethod()) yield ` ${line}`; 528 | yield ""; 529 | for (const line of this.fromJSONMethod()) yield ` ${line}`; 530 | yield ""; 531 | for (const line of this.toBytesMethod()) yield ` ${line}`; 532 | yield ""; 533 | for (const line of this.toJSONMethod()) yield ` ${line}`; 534 | yield `}`; 535 | } 536 | 537 | toString() { 538 | return [...this].join("\n"); 539 | } 540 | } 541 | 542 | interface ProtoGeneratorOpts { 543 | mod: string; 544 | protoPath: string; 545 | } 546 | 547 | class ProtoScanner implements Visitor { 548 | enums: Set = new Set(); 549 | messages: Set = new Set(); 550 | constructor(proto: Proto) { 551 | proto.accept(this); 552 | } 553 | 554 | visitEnum(node: Enum) { 555 | this.enums.add(node.name); 556 | } 557 | 558 | visitMessage(node: Message) { 559 | this.messages.add(node.name); 560 | } 561 | } 562 | 563 | class ProtoGenerator implements Visitor { 564 | syntax: 2 | 3 = 3; 565 | mod: string; 566 | protoPath: string; 567 | imports = new ImportMap(); 568 | dependencies: Set = new Set(); 569 | scopedMessages: Map> = new Map(); 570 | scopedEnums: Map> = new Map(); 571 | messages: Map = new Map(); 572 | enums: Map = new Map(); 573 | 574 | constructor( 575 | private proto: Proto, 576 | { mod, protoPath }: ProtoGeneratorOpts, 577 | ) { 578 | this.mod = mod; 579 | this.protoPath = protoPath; 580 | proto.accept(this); 581 | } 582 | 583 | visitImport(node: Import) { 584 | this.dependencies.add(node); 585 | } 586 | 587 | async collectScopes() { 588 | for (const node of this.dependencies) { 589 | const { messages, enums } = await scan(join(this.protoPath, node.source)); 590 | this.scopedEnums.set( 591 | "./" + node.source.replace(/.proto$/, ".pb.ts"), 592 | enums, 593 | ); 594 | this.scopedMessages.set( 595 | "./" + node.source.replace(/.proto$/, ".pb.ts"), 596 | messages, 597 | ); 598 | } 599 | } 600 | 601 | visitEnum(node: Enum) { 602 | this.enums.set(node.name, new EnumGenerator(node)); 603 | } 604 | 605 | visitSyntax(node: Syntax) { 606 | if (node.version === 2) { 607 | throw new Error(`syntax 2 protos are not parseable right now.`); 608 | } 609 | } 610 | 611 | visitMessage(message: Message) { 612 | this.messages.set(message.name, new MessageGenerator(message, this)); 613 | } 614 | 615 | private *body(): Generator { 616 | for (const node of this.enums.values()) { 617 | yield ""; 618 | for (const line of node) yield line; 619 | } 620 | for (const node of this.messages.values()) { 621 | yield ""; 622 | for (const line of node) yield line; 623 | } 624 | } 625 | 626 | *[Symbol.iterator](): Generator { 627 | yield `// Generated by protod v${version}`; 628 | for (const comment of this.proto.comments) { 629 | yield comment.body; 630 | } 631 | if (this.proto.comments.length) yield ""; 632 | // We need to consume the body to collect any side-effectful imports 633 | const body = [...this.body()]; 634 | for (const line of this.imports) yield line; 635 | for (const line of body) yield line; 636 | yield ""; 637 | } 638 | 639 | toString(): string { 640 | return [...this].join("\n") + "\n"; 641 | } 642 | } 643 | 644 | async function scan(path: string) { 645 | const file = await Deno.open(path, { read: true }); 646 | try { 647 | return new ProtoScanner(await parse(file, {})); 648 | } finally { 649 | file.close(); 650 | } 651 | } 652 | 653 | const defaultMod = `https://deno.land/x/protod@v${version}/mod.ts`; 654 | export async function generate( 655 | path: string, 656 | opts: Partial = {}, 657 | ): Promise { 658 | const file = await Deno.open(path, { read: true }); 659 | try { 660 | const proto = new ProtoGenerator( 661 | await parse(file, { comments: true }), 662 | Object.assign({ 663 | mod: defaultMod, 664 | protoPath: dirname(path), 665 | } as ProtoGeneratorOpts, opts), 666 | ); 667 | await proto.collectScopes(); 668 | return proto.toString(); 669 | } finally { 670 | file.close(); 671 | } 672 | } 673 | --------------------------------------------------------------------------------