Skip to content

Commit

Permalink
feat: Support decoding protobuf without defaults
Browse files Browse the repository at this point in the history
The protobuf specification demands type-specific default values such as
0 for numbers when decoding. That is quite weird for TypeScript code
which usually uses null or undefined for missing values. Let's create an
alternative way to decode without defaults.

Maybe the following issue will be resolved at some point so we would not
need our own workaround:
protobufjs/protobuf.js#1572
  • Loading branch information
haphut committed Dec 13, 2023
1 parent 9c2cb3d commit 0ce2fa4
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/util/protobufUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type protobufjs from "protobufjs/minimal";

/**
* The protobuf specification demands type-specific default values such as 0 for
* numbers when decoding. That is quite weird for TypeScript code which usually
* uses null or undefined for missing values. Let's create an alternative way to
* decode without defaults.
*
* Maybe the following issue will be resolved at some point so we would not need
* our own workaround:
* https://github.com/protobufjs/protobuf.js/issues/1572
*/

interface ProtobufMessageType<T> {
decode: (reader: protobufjs.Reader | Uint8Array, length?: number) => T;
toObject: (
message: T,
options?: protobufjs.IConversionOptions,
) => Record<string, unknown>;
}

const decodeWithoutDefaults = <T>(
messageType: ProtobufMessageType<T>,
payload: Uint8Array,
): T =>
messageType.toObject(messageType.decode(payload), {
longs: Number,
defaults: false,
}) as T;

export default decodeWithoutDefaults;
97 changes: 97 additions & 0 deletions tests/util/protobufUtil.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { hfp } from "../../src/protobuf/hfp";
import { mqtt } from "../../src/protobuf/mqtt";
import decodeWithoutDefaults from "../../src/util/protobufUtil";

describe("decodeWithoutDefaults", () => {
test("decode HFP message with payload stop 'null'", () => {
const hfpData = {
SchemaVersion: 1,
topic: {
SchemaVersion: 1,
receivedAt: new Date("2023-10-23T13:42:42.794243Z").getTime(),
topicPrefix: "/hfp/",
topicVersion: "v2",
journeyType: hfp.Topic.JourneyType.journey,
temporalType: hfp.Topic.TemporalType.ongoing,
eventType: hfp.Topic.EventType.VP,
transportMode: hfp.Topic.TransportMode.bus,
operatorId: 18,
vehicleNumber: 1003,
uniqueVehicleId: "18/1003",
routeId: "5520",
directionId: 2,
headsign: "Matinkylä (M)",
startTime: "15:56",
nextStop: "2323253",
geohashLevel: 4,
latitude: 60.168,
longitude: 24.734,
},
payload: {
SchemaVersion: 1,
desi: "520",
dir: "2",
oper: 6,
veh: 1003,
tst: "2023-10-23T13:42:42.728Z",
tsi: 1698068562,
spd: 8.49,
hdg: 156,
lat: 60.168786,
long: 24.734465,
acc: -1.29,
dl: -101,
odo: 20729,
drst: 0,
oday: "2023-10-23",
jrn: 812,
line: 1110,
start: "15:56",
loc: hfp.Payload.LocationQualityMethod.GPS,
stop: null,
route: "5520",
occu: 0,
},
};
const verificationErrorMessage = hfp.Data.verify(hfpData);
expect(verificationErrorMessage).toBeNull();
const encoded = hfp.Data.encode(hfp.Data.create(hfpData)).finish();
const withDefaults = hfp.Data.decode(encoded);
const noDefaults = decodeWithoutDefaults(hfp.Data, encoded);
expect(withDefaults.payload).toHaveProperty("stop");
expect(withDefaults.payload.stop).toBeDefined();
expect(withDefaults.payload.stop).toStrictEqual(0);
expect(withDefaults.payload.loc).toStrictEqual(expect.any(Number));
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(noDefaults.payload.stop).toBeUndefined();
expect(noDefaults.payload).not.toHaveProperty("stop");
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(noDefaults.payload.loc).toStrictEqual(expect.any(Number));
// Change Longs to Numbers so that toMatchObject succeeds.
withDefaults.payload.tsi = Number(withDefaults.payload.tsi);
if (withDefaults.topic?.receivedAt != null) {
withDefaults.topic.receivedAt = Number(withDefaults.topic.receivedAt);
}
expect(withDefaults).toMatchObject(noDefaults);
});

test("decode MQTT message with missing topic", () => {
const mqttMessage = {
SchemaVersion: 1,
payload: Buffer.from("foo", "utf8"),
};
const verificationErrorMessage = mqtt.RawMessage.verify(mqttMessage);
expect(verificationErrorMessage).toBeNull();
const encoded = mqtt.RawMessage.encode(
mqtt.RawMessage.create(mqttMessage),
).finish();
const withDefaults = mqtt.RawMessage.decode(encoded);
const noDefaults = decodeWithoutDefaults(mqtt.RawMessage, encoded);
expect(withDefaults).toHaveProperty("topic");
expect(withDefaults.topic).toBeDefined();
expect(withDefaults.topic).toStrictEqual("");
expect(noDefaults).not.toHaveProperty("topic");
expect(noDefaults.topic).toBeUndefined();
expect(withDefaults).toMatchObject(noDefaults);
});
});

0 comments on commit 0ce2fa4

Please sign in to comment.