Skip to content

Commit

Permalink
feat: implement well-known-type overrides and extensions
Browse files Browse the repository at this point in the history
Signed-off-by: Christian Stewart <[email protected]>
  • Loading branch information
paralin committed Apr 30, 2024
1 parent a3ef494 commit 4a8c6cd
Show file tree
Hide file tree
Showing 21 changed files with 1,938 additions and 653 deletions.
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
## protobuf-es-lite

[![GoDoc Widget]][GoDoc] [![Go Report Card Widget]][Go Report Card]

[GoDoc]: https://godoc.org/github.com/aperturerobotics/protobuf-es-lite
[GoDoc Widget]: https://godoc.org/github.com/aperturerobotics/protobuf-es-lite?status.svg
[Go Report Card Widget]: https://goreportcard.com/badge/github.com/aperturerobotics/protobuf-es-lite
[Go Report Card]: https://goreportcard.com/report/github.com/aperturerobotics/protobuf-es-lite
[![npm version](https://img.shields.io/npm/v/@aptre/protobuf-es-lite.svg)](https://www.npmjs.com/package/@aptre/protobuf-es-lite)
[![npm downloads](https://img.shields.io/npm/dm/@aptre/protobuf-es-lite.svg)](https://www.npmjs.com/package/@aptre/protobuf-es-lite)

protobuf-es-lite is a TypeScript and JavaScript protobuf implementation.

Expand Down
13 changes: 6 additions & 7 deletions example/example.pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/* eslint-disable */

import type { MessageType, PartialFieldInfo } from "@aptre/protobuf-es-lite";
import { createEnumType, createMessageType, Message, Timestamp } from "@aptre/protobuf-es-lite";
import { createEnumType, createMessageType, Message, ScalarType, Timestamp } from "@aptre/protobuf-es-lite";

export const protobufPackage = "example";

Expand Down Expand Up @@ -77,17 +77,16 @@ export type EchoMsg = Message<{

}>;

export const EchoMsg: MessageType<EchoMsg> = createMessageType(
{
// EchoMsg contains the message type declaration for EchoMsg.
export const EchoMsg: MessageType<EchoMsg> = createMessageType({
typeName: "example.EchoMsg",
fields: [
{ no: 1, name: "body", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 1, name: "body", kind: "scalar", T: ScalarType.STRING },
{ no: 2, name: "ts", kind: "message", T: () => Timestamp },
{ no: 3, name: "example_enum", kind: "enum", T: ExampleEnum_Enum, oneof: "demo" },
{ no: 4, name: "example_string", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "demo" },
{ no: 4, name: "example_string", kind: "scalar", T: ScalarType.STRING, oneof: "demo" },
{ no: 5, name: "timestamps", kind: "message", T: () => Timestamp, repeated: true },
] as readonly PartialFieldInfo[],
packedByDefault: true,
},
);
});

79 changes: 45 additions & 34 deletions src/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,31 @@ export interface BinaryWriteOptions {
writerFactory: () => IBinaryWriter;
}

export function readField(
// Default options for parsing binary data.
const readDefaults: Readonly<BinaryReadOptions> = {
readUnknownFields: true,
readerFactory: (bytes) => new BinaryReader(bytes),
};

// Default options for serializing binary data.
const writeDefaults: Readonly<BinaryWriteOptions> = {
writeUnknownFields: true,
writerFactory: () => new BinaryWriter(),
};

function makeReadOptions(
options?: Partial<BinaryReadOptions>,
): Readonly<BinaryReadOptions> {
return options ? { ...readDefaults, ...options } : readDefaults;
}

function makeWriteOptions(
options?: Partial<BinaryWriteOptions>,
): Readonly<BinaryWriteOptions> {
return options ? { ...writeDefaults, ...options } : writeDefaults;
}

function readField(
target: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any -- `any` is the best choice for dynamic access
reader: IBinaryReader,
field: FieldInfo,
Expand Down Expand Up @@ -154,7 +178,7 @@ type AnyMessage = {
};

// Read a map field, expecting key field = 1, value field = 2
export function readMapEntry(
function readMapEntry(
field: FieldInfo & { kind: "map" },
reader: IBinaryReader,
options: BinaryReadOptions,
Expand Down Expand Up @@ -213,10 +237,7 @@ export function readMapEntry(
return [key, val];
}

export function readScalar(
reader: IBinaryReader,
type: ScalarType,
): ScalarValue {
function readScalar(reader: IBinaryReader, type: ScalarType): ScalarValue {
switch (type) {
case ScalarType.STRING:
return reader.string();
Expand Down Expand Up @@ -253,7 +274,7 @@ export function readScalar(

// Read a scalar value, but return 64 bit integral types (int64, uint64,
// sint64, fixed64, sfixed64) as string instead of bigint.
export function readScalarLTString(
function readScalarLTString(
reader: IBinaryReader,
type: ScalarType,
): Exclude<ScalarValue, bigint> {
Expand Down Expand Up @@ -282,31 +303,7 @@ function readMessageField<T>(
return message;
}

// Default options for parsing binary data.
const readDefaults: Readonly<BinaryReadOptions> = {
readUnknownFields: true,
readerFactory: (bytes) => new BinaryReader(bytes),
};

// Default options for serializing binary data.
const writeDefaults: Readonly<BinaryWriteOptions> = {
writeUnknownFields: true,
writerFactory: () => new BinaryWriter(),
};

export function makeReadOptions(
options?: Partial<BinaryReadOptions>,
): Readonly<BinaryReadOptions> {
return options ? { ...readDefaults, ...options } : readDefaults;
}

export function makeWriteOptions(
options?: Partial<BinaryWriteOptions>,
): Readonly<BinaryWriteOptions> {
return options ? { ...writeDefaults, ...options } : writeDefaults;
}

export function readMessage<T>(
function readMessage<T>(
message: T,
fields: FieldList,
reader: IBinaryReader,
Expand Down Expand Up @@ -344,7 +341,7 @@ export function readMessage<T>(
/**
* Serialize a message to binary data.
*/
export function writeMessage<T>(
function writeMessage<T>(
message: T,
fields: FieldList,
writer: IBinaryWriter,
Expand Down Expand Up @@ -515,7 +512,7 @@ function scalarTypeInfo(
return [wireType, method];
}

export function writeMapEntry(
function writeMapEntry(
writer: IBinaryWriter,
options: BinaryWriteOptions,
field: FieldInfo & { kind: "map" },
Expand Down Expand Up @@ -565,3 +562,17 @@ export function writeMapEntry(

writer.join();
}
export {
readField as binaryReadField,
readMapEntry as binaryReadMapEntry,
readScalar as binaryReadScalar,
readScalarLTString as binaryReadScalarLTString,
readMessage as binaryReadMessage,
writeField as binaryWriteField,
writeScalar as binaryWriteScalar,
writePacked as binaryWritePacked,
writeMapEntry as binaryWriteMapEntry,
writeMessage as binaryWriteMessage,
makeReadOptions as binaryMakeReadOptions,
makeWriteOptions as binaryMakeWriteOptions,
};
16 changes: 12 additions & 4 deletions src/codegen-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,24 @@ const symbols = {
PartialFieldInfo: symbolInfo(true, "./field.js"),
MessageType: symbolInfo(true, "./message-type.js"),
Extension: symbolInfo(true, "./extension.js"),
IMessageTypeRegistry: symbolInfo(true, "./type-registry.js"),
BinaryReadOptions: symbolInfo(true, "./binary-format.js"),
BinaryWriteOptions: symbolInfo(true, "./binary-format.js"),
JsonReadOptions: symbolInfo(true, "./json-format.js"),
JsonWriteOptions: symbolInfo(true, "./json-format.js"),
JsonValue: symbolInfo(true, "./json-format.js"),
JsonObject: symbolInfo(true, "./json-format.js"),
JsonReadOptions: symbolInfo(true, "./json.js"),
JsonWriteOptions: symbolInfo(true, "./json.js"),
JsonValue: symbolInfo(true, "./json.js"),
JsonObject: symbolInfo(true, "./json.js"),
jsonReadEnum: symbolInfo(false, "./json.js"),
jsonReadScalar: symbolInfo(false, "./json.js"),
jsonWriteEnum: symbolInfo(false, "./json.js"),
jsonWriteScalar: symbolInfo(false, "./json.js"),
jsonDebugValue: symbolInfo(false, "./json.js"),
protoDouble: symbolInfo(false, "./proto-double.js"),
protoInt64: symbolInfo(false, "./proto-int64.js"),
applyPartialMessage: symbolInfo(false, "./partial.js"),
ScalarType: symbolInfo(false, "./scalar.js"),
LongType: symbolInfo(false, "./scalar.js"),
ScalarValue: symbolInfo(false, "./scalar.js"),
MethodKind: symbolInfo(false, "./service-type.js"),
MethodIdempotency: symbolInfo(false, "./service-type.js"),
createEnumType: symbolInfo(false, "./enum.js"),
Expand Down
16 changes: 8 additions & 8 deletions src/descriptor-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export type DescExtension = DescFieldCommon &
readonly extendee: DescMessage;
};

interface DescFieldCommon {
export interface DescFieldCommon {
/**
* The field name, as specified in the protobuf source
*/
Expand Down Expand Up @@ -426,7 +426,7 @@ interface DescFieldCommon {
toString(): string;
}

interface DescFieldScalar {
export interface DescFieldScalar {
readonly fieldKind: "scalar";
/**
* Is the field repeated?
Expand Down Expand Up @@ -471,7 +471,7 @@ interface DescFieldScalar {
| undefined;
}

interface DescFieldMessage {
export interface DescFieldMessage {
readonly fieldKind: "message";
/**
* Is the field repeated?
Expand Down Expand Up @@ -504,7 +504,7 @@ interface DescFieldMessage {
readonly mapValue: undefined;
}

interface DescFieldEnum {
export interface DescFieldEnum {
readonly fieldKind: "enum";
/**
* Is the field repeated?
Expand Down Expand Up @@ -550,7 +550,7 @@ interface DescFieldEnum {
| undefined;
}

interface DescFieldMap {
export interface DescFieldMap {
readonly fieldKind: "map";
/**
* Is the field repeated?
Expand Down Expand Up @@ -589,7 +589,7 @@ interface DescFieldMap {
| DescFieldMapValueScalar;
}

interface DescFieldMapValueEnum {
export interface DescFieldMapValueEnum {
readonly kind: "enum";
/**
* The enum type, if this is a map field with enum values.
Expand All @@ -605,7 +605,7 @@ interface DescFieldMapValueEnum {
readonly scalar: undefined;
}

interface DescFieldMapValueMessage {
export interface DescFieldMapValueMessage {
readonly kind: "message";
/**
* The enum type, if this is a map field with enum values.
Expand All @@ -621,7 +621,7 @@ interface DescFieldMapValueMessage {
readonly scalar: undefined;
}

interface DescFieldMapValueScalar {
export interface DescFieldMapValueScalar {
readonly kind: "scalar";
/**
* The enum type, if this is a map field with enum values.
Expand Down
89 changes: 81 additions & 8 deletions src/google/protobuf/any.pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
// @generated from file google/protobuf/any.proto (package google.protobuf, syntax proto3)
/* eslint-disable */

import type { MessageType, PartialFieldInfo } from "../../index.js";
import { createMessageType, Message } from "../../index.js";
import type { IMessageTypeRegistry, JsonReadOptions, JsonValue, JsonWriteOptions, MessageType, PartialFieldInfo } from "../../index.js";
import { applyPartialMessage, createMessageType, Message, ScalarType } from "../../index.js";

export const protobufPackage = "google.protobuf";

Expand Down Expand Up @@ -171,14 +171,87 @@ export type Any = Message<{

}>;

export const Any: MessageType<Any> = createMessageType(
{
// Any_Wkt contains the well-known-type overrides for Any.
const Any_Wkt = {
toJson(msg: Any, options?: Partial<JsonWriteOptions>): JsonValue {
const typeName = msg?.typeUrl;
if (!typeName) {
return {};
}
const messageType = options?.typeRegistry?.findMessage(typeName);
if (!messageType) {
throw new Error(`cannot encode message google.protobuf.Any to JSON: "${typeName}" is not in the type registry`);
}
const message = messageType.fromBinary(msg.value);
let json = messageType.toJson(message, options);
if (typeName.startsWith("google.protobuf.") || (json === null || Array.isArray(json) || typeof json !== "object")) {
json = {value: json};
}
json["@type"] = typeName;
return json;
},

fromJson(json: JsonValue, options?: Partial<JsonReadOptions>) {
if (json === null || Array.isArray(json) || typeof json != "object") {
throw new Error(`cannot decode message google.protobuf.Any from JSON: expected object but got ${json === null ? "null" : Array.isArray(json) ? "array" : typeof json}`);
}
if (Object.keys(json).length == 0) {
return {} as Any;
}
const typeUrl = json["@type"];
if (typeof typeUrl != "string" || typeUrl == "") {
throw new Error(`cannot decode message google.protobuf.Any from JSON: "@type" is empty`);
}
const typeName = typeUrl, messageType = options?.typeRegistry?.findMessage(typeName);
if (!messageType) {
throw new Error(`cannot decode message google.protobuf.Any from JSON: ${typeUrl} is not in the type registry`);
}
let message;
if (typeName.startsWith("google.protobuf.") && Object.prototype.hasOwnProperty.call(json, "value")) {
message = messageType.fromJson(json["value"], options);
} else {
const copy = Object.assign({}, json);
delete copy["@type"];
message = messageType.fromJson(copy, options);
}
const out = {} as Any;
Any.packFrom(out, message, messageType);
return out;
},

packFrom<T extends Message<T>>(out: Any, message: Message<T>, messageType: MessageType<T>): void {
out.value = messageType.toBinary(message);
out.typeUrl = messageType.typeName;
},

unpackTo<T extends Message<T>>(msg: Any, target: Message<T>, targetMessageType: MessageType<T>): boolean {
if (!Any.is(msg, targetMessageType)) {
return false;
}
const partial = targetMessageType.fromBinary(msg.value);
applyPartialMessage(partial, target, targetMessageType.fields);
return true;
},

unpack<T extends Message<T>>(msg: Any, registry: IMessageTypeRegistry): {message: Message<T>, messageType: MessageType<T>} | undefined {
const typeUrl = msg.typeUrl
const messageType = !!typeUrl && registry.findMessage<T>(typeUrl);
return messageType ? {message: messageType.fromBinary(msg.value), messageType} : undefined;
},

is(msg: Any, msgType: MessageType | string): boolean {
const name = msg.typeUrl
return !!name && (typeof msgType === 'string' ? name === msgType : name === msgType.typeName);
},
};

// Any contains the message type declaration for Any.
export const Any: MessageType<Any> & typeof Any_Wkt = createMessageType<Any, typeof Any_Wkt>({
typeName: "google.protobuf.Any",
fields: [
{ no: 1, name: "type_url", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "value", kind: "scalar", T: 12 /* ScalarType.BYTES */ },
{ no: 1, name: "type_url", kind: "scalar", T: ScalarType.STRING },
{ no: 2, name: "value", kind: "scalar", T: ScalarType.BYTES },
] as readonly PartialFieldInfo[],
packedByDefault: true,
},
);
}, Any_Wkt);

Loading

0 comments on commit 4a8c6cd

Please sign in to comment.