Skip to content

Commit

Permalink
Add toPlainMessage (#511)
Browse files Browse the repository at this point in the history
Add `toPlainMessage` to convert `Message` objects to their
`PlainMessage` variants.
  • Loading branch information
srikrsna-buf authored Jun 28, 2023
1 parent f955b29 commit e279d20
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/protobuf-test/extra/enum.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ message EnumMessage {
NESTED_ZERO = 0;
NESTED_ONE = 1;
}
NestedEnum enum_field = 1;
}

enum SimpleEnum {
Expand Down
5 changes: 5 additions & 0 deletions packages/protobuf-test/src/gen/js/extra/enum_pb.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/protobuf-test/src/gen/js/extra/enum_pb.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/protobuf-test/src/gen/ts/extra/enum_pb.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

268 changes: 268 additions & 0 deletions packages/protobuf-test/src/to-plain-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// Copyright 2021-2023 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { describe, expect, test } from "@jest/globals";
import { ScalarValuesMessage } from "./gen/ts/extra/msg-scalar_pb.js";
import { toPlainMessage, protoInt64 } from "@bufbuild/protobuf";
import type { PlainMessage } from "@bufbuild/protobuf";
import {
OneofEnum,
OneofMessage,
OneofMessageFoo,
} from "./gen/ts/extra/msg-oneof_pb.js";
import { MapsEnum, MapsMessage } from "./gen/ts/extra/msg-maps_pb.js";
import { MessageFieldMessage } from "./gen/ts/extra/msg-message_pb.js";
import { EnumMessage, EnumMessage_NestedEnum } from "./gen/ts/extra/enum_pb.js";
import { WrappersMessage } from "./gen/ts/extra/wkt-wrappers_pb.js";

describe("toPlainMessage", () => {
describe("on scalar", () => {
test("returns unset defaults", () => {
const defaultValue: PlainMessage<ScalarValuesMessage> = {
boolField: false,
bytesField: new Uint8Array(),
doubleField: 0,
fixed32Field: 0,
fixed64Field: protoInt64.zero,
floatField: 0,
int32Field: 0,
int64Field: protoInt64.zero,
sfixed32Field: 0,
sfixed64Field: protoInt64.zero,
sint32Field: 0,
sint64Field: protoInt64.zero,
stringField: "",
uint32Field: 0,
uint64Field: protoInt64.zero,
};
const act = toPlainMessage(new ScalarValuesMessage({}));
expect(act).toEqual(defaultValue);
expectPlainObject(act);
});
test("returns set fields", () => {
const exp = {
boolField: true,
bytesField: new Uint8Array([1]),
doubleField: 1.2,
fixed32Field: 1,
fixed64Field: protoInt64.parse(1),
floatField: 1,
int32Field: 1,
int64Field: protoInt64.parse(1),
sfixed32Field: 1,
sfixed64Field: protoInt64.parse(1),
sint32Field: 1,
sint64Field: protoInt64.parse(1),
stringField: "some",
uint32Field: 1,
uint64Field: protoInt64.parse(1),
};
const act = toPlainMessage(new ScalarValuesMessage(exp));
expect(act).toEqual(exp);
expectPlainObject(act);
});
});
describe("on enums", () => {
test("returns unset defaults", () => {
const act = toPlainMessage(new EnumMessage({}));
expect(act).toEqual({
enumField: EnumMessage_NestedEnum.NESTED_ZERO,
});
expectPlainObject(act);
});
});
describe("on oneof", () => {
test("when not set", () => {
const act = toPlainMessage(new OneofMessage({}));
expect(act).toEqual({
enum: { case: undefined },
message: { case: undefined },
scalar: { case: undefined },
});
expectPlainObject(act);
});
test("with enums", () => {
const act = toPlainMessage(
new OneofMessage({
enum: {
case: "e",
value: OneofEnum.A,
},
})
);
expect(act).toEqual({
enum: { case: "e", value: OneofEnum.A },
message: { case: undefined },
scalar: { case: undefined },
});
expectPlainObject(act);
});
test("with messages", () => {
const act = toPlainMessage(
new OneofMessage({
message: {
case: "foo",
value: new OneofMessageFoo(),
},
})
);
expect(act).toEqual({
message: {
case: "foo",
value: {
name: "",
toggle: false,
},
},
enum: { case: undefined },
scalar: { case: undefined },
});
expectPlainObject(act);
});
test("with scalars", () => {
const act = toPlainMessage(
new OneofMessage({
scalar: {
case: "value",
value: 1,
},
})
);
expect(act).toEqual({
scalar: { case: "value", value: 1 },
message: { case: undefined },
enum: { case: undefined },
});
expectPlainObject(act);
});
});
describe("on maps", () => {
const defaultValue: PlainMessage<MapsMessage> = {
strStrField: {},
strInt32Field: {},
strInt64Field: {},
strBoolField: {},
strBytesField: {},
int32StrField: {},
int64StrField: {},
boolStrField: {},
strMsgField: {},
int32MsgField: {},
int64MsgField: {},
strEnuField: {},
int32EnuField: {},
int64EnuField: {},
};
test("returns unset defaults", () => {
const act = toPlainMessage(new MapsMessage({}));
expect(act).toEqual(defaultValue);
expectPlainObject(act);
});
test("returns set fields", () => {
const exp = {
strStrField: { a: "str", b: "xx" },
strInt32Field: { a: 123, b: 455 },
strInt64Field: { a: protoInt64.parse(123) },
strBoolField: { a: true, b: false },
strBytesField: {
a: new Uint8Array([
104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100,
]),
},
int32StrField: { 123: "hello" },
int64StrField: { "9223372036854775807": "hello" },
boolStrField: { true: "yes", false: "no" },
strMsgField: {
a: defaultValue,
},
int32MsgField: {
1: defaultValue,
},
int64MsgField: {
"1": defaultValue,
},
strEnuField: { a: MapsEnum.ANY, b: MapsEnum.NO, c: MapsEnum.YES },
int32EnuField: { 1: MapsEnum.ANY, 2: MapsEnum.NO, 0: MapsEnum.YES },
int64EnuField: {
"-1": MapsEnum.ANY,
"2": MapsEnum.NO,
"0": MapsEnum.YES,
},
};
const act = toPlainMessage(new MapsMessage(exp));
expect(act).toEqual(exp);
expectPlainObject(act);
});
});
describe("on messages", () => {
test("returns unset defaults", () => {
const exp: PlainMessage<MessageFieldMessage> = {
messageField: undefined,
repeatedMessageField: [],
};
const act = toPlainMessage(new MessageFieldMessage(exp));
expect(act).toEqual(exp);
expectPlainObject(act);
});
test("returns set fields", () => {
const exp: PlainMessage<MessageFieldMessage> = {
messageField: { name: "" },
repeatedMessageField: [{ name: "" }],
};
const act = toPlainMessage(new MessageFieldMessage(exp));
expect(act).toEqual(exp);
expectPlainObject(act);
});
});
describe("on wrapper messages", () => {
const exp: PlainMessage<WrappersMessage> = {
mapStringValueField: {},
mapBoolValueField: {},
mapBytesValueField: {},
oneofFields: {
case: undefined,
},
repeatedDoubleValueField: [],
repeatedBoolValueField: [],
repeatedFloatValueField: [],
repeatedInt64ValueField: [],
repeatedUint64ValueField: [],
repeatedInt32ValueField: [],
repeatedUint32ValueField: [],
repeatedStringValueField: [],
repeatedBytesValueField: [],
mapDoubleValueField: {},
mapFloatValueField: {},
mapInt64ValueField: {},
mapUint64ValueField: {},
mapInt32ValueField: {},
mapUint32ValueField: {},
boolValueField: true,
bytesValueField: new Uint8Array(),
doubleValueField: 1.2,
floatValueField: 1.2,
int32ValueField: 1,
int64ValueField: protoInt64.parse(1),
stringValueField: "some",
uint32ValueField: 1,
uint64ValueField: protoInt64.uParse(1),
};
const act = toPlainMessage(new WrappersMessage(exp));
expect(act).toEqual(exp);
});
});

function expectPlainObject(value: unknown) {
expect(Object.getPrototypeOf(value)).toEqual(Object.prototype);
}
1 change: 1 addition & 0 deletions packages/protobuf/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export { createDescriptorSet } from "./create-descriptor-set.js";
export type { IMessageTypeRegistry } from "./type-registry.js";
export { createRegistry } from "./create-registry.js";
export { createRegistryFromDescriptors } from "./create-registry-from-desc.js";
export { toPlainMessage } from "./to-plain-message.js";

// ideally, we would export these types with sub-path exports:
export * from "./google/protobuf/compiler/plugin_pb.js";
Expand Down
67 changes: 67 additions & 0 deletions packages/protobuf/src/to-plain-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2021-2023 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-argument,no-case-declarations */

import { Message } from "./message.js";
import type { AnyMessage, PlainMessage } from "./message.js";

/**
* toPlainMessage returns a new object by striping
* all methods from a message, leaving only fields and
* oneof groups. It is recursive, meaning it applies this
* same logic to all nested message fields as well.
*/
export function toPlainMessage<T extends Message<T>>(
message: T
): PlainMessage<T> {
const type = message.getType();
const target = {} as AnyMessage;
for (const member of type.fields.byMember()) {
const source = (message as AnyMessage)[member.localName];
let copy: any;
if (member.repeated) {
copy = (source as any[]).map((e) => toPlainValue(e));
} else if (member.kind == "map") {
copy = {};
for (const [key, v] of Object.entries(source)) {
copy[key] = toPlainValue(v);
}
} else if (member.kind == "oneof") {
const f = member.findField(source.case);
copy = f
? { case: source.case, value: toPlainValue(source.value) }
: { case: undefined };
} else {
copy = toPlainValue(source);
}
target[member.localName] = copy;
}
return target as PlainMessage<T>;
}

function toPlainValue(value: any) {
if (value === undefined) {
return value;
}
if (value instanceof Message) {
return toPlainMessage(value);
}
if (value instanceof Uint8Array) {
const c = new Uint8Array(value.byteLength);
c.set(value);
return c;
}
return value;
}

0 comments on commit e279d20

Please sign in to comment.