From bb3460d71b2f2cd75f10efe94d739e15561c2ccf Mon Sep 17 00:00:00 2001 From: Daniel Kurka Date: Fri, 8 May 2020 13:06:17 -0700 Subject: [PATCH] Project import generated by Copybara PiperOrigin-RevId: 310614231 --- js/experimental/runtime/kernel/message_set.js | 285 ++++++++++++++++++ .../runtime/kernel/message_set_test.js | 262 ++++++++++++++++ .../runtime/testing/binary/test_message.js | 10 +- 3 files changed, 555 insertions(+), 2 deletions(-) create mode 100644 js/experimental/runtime/kernel/message_set.js create mode 100644 js/experimental/runtime/kernel/message_set_test.js diff --git a/js/experimental/runtime/kernel/message_set.js b/js/experimental/runtime/kernel/message_set.js new file mode 100644 index 0000000000..d66bace7b5 --- /dev/null +++ b/js/experimental/runtime/kernel/message_set.js @@ -0,0 +1,285 @@ +/* +########################################################## +# # +# __ __ _____ _ _ _____ _ _ _____ # +# \ \ / /\ | __ \| \ | |_ _| \ | |/ ____| # +# \ \ /\ / / \ | |__) | \| | | | | \| | | __ # +# \ \/ \/ / /\ \ | _ /| . ` | | | | . ` | | |_ | # +# \ /\ / ____ \| | \ \| |\ |_| |_| |\ | |__| | # +# \/ \/_/ \_\_| \_\_| \_|_____|_| \_|\_____| # +# # +# # +########################################################## +# Do not use this class in your code. This class purely # +# exists to make proto code generation easier. # +########################################################## +*/ +goog.module('protobuf.runtime.MessageSet'); + +const InternalMessage = goog.require('protobuf.binary.InternalMessage'); +const Kernel = goog.require('protobuf.runtime.Kernel'); + +// These are the tags for the old MessageSet format, which was defined as: +// message MessageSet { +// repeated group Item = 1 { +// required uint32 type_id = 2; +// optional bytes message = 3; +// } +// } +/** @const {number} */ +const MSET_GROUP_FIELD_NUMBER = 1; +/** @const {number} */ +const MSET_TYPE_ID_FIELD_NUMBER = 2; +/** @const {number} */ +const MSET_MESSAGE_FIELD_NUMBER = 3; + +/** + * @param {!Kernel} kernel + * @return {!Map} + */ +function createItemMap(kernel) { + const itemMap = new Map(); + let totalCount = 0; + for (const item of kernel.getRepeatedGroupIterable( + MSET_GROUP_FIELD_NUMBER, Item.fromKernel)) { + itemMap.set(item.getTypeId(), item); + totalCount++; + } + + // Normalize the entries. + if (totalCount > itemMap.size) { + writeItemMap(kernel, itemMap); + } + return itemMap; +} + +/** + * @param {!Kernel} kernel + * @param {!Map} itemMap + */ +function writeItemMap(kernel, itemMap) { + kernel.setRepeatedGroupIterable(MSET_GROUP_FIELD_NUMBER, itemMap.values()); +} + +/** + * @implements {InternalMessage} + * @final + */ +class MessageSet { + /** + * @param {!Kernel} kernel + * @return {!MessageSet} + */ + static fromKernel(kernel) { + const itemMap = createItemMap(kernel); + return new MessageSet(kernel, itemMap); + } + + /** + * @return {!MessageSet} + */ + static createEmpty() { + return MessageSet.fromKernel(Kernel.createEmpty()); + } + + /** + * @param {!Kernel} kernel + * @param {!Map} itemMap + * @private + */ + constructor(kernel, itemMap) { + /** @const {!Kernel} @private */ + this.kernel_ = kernel; + /** @const {!Map} @private */ + this.itemMap_ = itemMap; + } + + + + // code helpers for code gen + + /** + * @param {number} typeId + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {?T} + * @template T + */ + getMessageOrNull(typeId, instanceCreator, pivot) { + const item = this.itemMap_.get(typeId); + return item ? item.getMessageOrNull(instanceCreator, pivot) : null; + } + + /** + * @param {number} typeId + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {T} + * @template T + */ + getMessageAttach(typeId, instanceCreator, pivot) { + let item = this.itemMap_.get(typeId); + if (item) { + return item.getMessageAttach(instanceCreator, pivot); + } + const message = instanceCreator(Kernel.createEmpty()); + this.setMessage(typeId, message); + return message; + } + + /** + * @param {number} typeId + * @param {number=} pivot + * @return {?Kernel} + */ + getMessageAccessorOrNull(typeId, pivot) { + const item = this.itemMap_.get(typeId); + return item ? item.getMessageAccessorOrNull(pivot) : null; + } + + + /** + * @param {number} typeId + */ + clearMessage(typeId) { + if (this.itemMap_.delete(typeId)) { + writeItemMap(this.kernel_, this.itemMap_); + } + } + + /** + * @param {number} typeId + * @return {boolean} + */ + hasMessage(typeId) { + return this.itemMap_.has(typeId); + } + + /** + * @param {number} typeId + * @param {!InternalMessage} value + */ + setMessage(typeId, value) { + const item = this.itemMap_.get(typeId); + if (item) { + item.setMessage(value); + } else { + this.itemMap_.set(typeId, Item.create(typeId, value)); + writeItemMap(this.kernel_, this.itemMap_); + } + } + + /** + * @return {!Kernel} + * @override + */ + internalGetKernel() { + return this.kernel_; + } +} + +/** + * @implements {InternalMessage} + * @final + */ +class Item { + /** + * @param {number} typeId + * @param {!InternalMessage} message + * @return {!Item} + */ + static create(typeId, message) { + const messageSet = Item.fromKernel(Kernel.createEmpty()); + messageSet.setTypeId_(typeId); + messageSet.setMessage(message); + return messageSet; + } + + + /** + * @param {!Kernel} kernel + * @return {!Item} + */ + static fromKernel(kernel) { + return new Item(kernel); + } + + /** + * @param {!Kernel} kernel + * @private + */ + constructor(kernel) { + /** @const {!Kernel} @private */ + this.kernel_ = kernel; + } + + /** + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {T} + * @template T + */ + getMessage(instanceCreator, pivot) { + return this.kernel_.getMessage( + MSET_MESSAGE_FIELD_NUMBER, instanceCreator, pivot); + } + + /** + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {?T} + * @template T + */ + getMessageOrNull(instanceCreator, pivot) { + return this.kernel_.getMessageOrNull( + MSET_MESSAGE_FIELD_NUMBER, instanceCreator, pivot); + } + + /** + * @param {function(!Kernel):T} instanceCreator + * @param {number=} pivot + * @return {T} + * @template T + */ + getMessageAttach(instanceCreator, pivot) { + return this.kernel_.getMessageAttach( + MSET_MESSAGE_FIELD_NUMBER, instanceCreator, pivot); + } + + /** + * @param {number=} pivot + * @return {?Kernel} + */ + getMessageAccessorOrNull(pivot) { + return this.kernel_.getMessageAccessorOrNull( + MSET_MESSAGE_FIELD_NUMBER, pivot); + } + + /** @param {!InternalMessage} value */ + setMessage(value) { + this.kernel_.setMessage(MSET_MESSAGE_FIELD_NUMBER, value); + } + + /** @return {number} */ + getTypeId() { + return this.kernel_.getUint32WithDefault(MSET_TYPE_ID_FIELD_NUMBER); + } + + /** + * @param {number} value + * @private + */ + setTypeId_(value) { + this.kernel_.setUint32(MSET_TYPE_ID_FIELD_NUMBER, value); + } + + /** + * @return {!Kernel} + * @override + */ + internalGetKernel() { + return this.kernel_; + } +} + +exports = MessageSet; diff --git a/js/experimental/runtime/kernel/message_set_test.js b/js/experimental/runtime/kernel/message_set_test.js new file mode 100644 index 0000000000..35e5935015 --- /dev/null +++ b/js/experimental/runtime/kernel/message_set_test.js @@ -0,0 +1,262 @@ +/** + * @fileoverview Tests for message_set.js. + */ +goog.module('protobuf.runtime.MessageSetTest'); + +goog.setTestOnly(); + +const Kernel = goog.require('protobuf.runtime.Kernel'); +const MessageSet = goog.require('protobuf.runtime.MessageSet'); +const TestMessage = goog.require('protobuf.testing.binary.TestMessage'); + +/** + * @param {...number} bytes + * @return {!ArrayBuffer} + */ +function createArrayBuffer(...bytes) { + return new Uint8Array(bytes).buffer; +} + +describe('MessageSet does', () => { + it('returns no messages for empty set', () => { + const messageSet = MessageSet.createEmpty(); + expect(messageSet.getMessageOrNull(12345, TestMessage.instanceCreator)) + .toBeNull(); + }); + + it('returns no kernel for empty set', () => { + const messageSet = MessageSet.createEmpty(); + expect(messageSet.getMessageAccessorOrNull(12345)).toBeNull(); + }); + + it('returns message that has been set', () => { + const messageSet = MessageSet.createEmpty(); + const message = TestMessage.createEmpty(); + messageSet.setMessage(12345, message); + expect(messageSet.getMessageOrNull(12345, TestMessage.instanceCreator)) + .toBe(message); + }); + + it('returns null for cleared message', () => { + const messageSet = MessageSet.createEmpty(); + const message = TestMessage.createEmpty(); + messageSet.setMessage(12345, message); + messageSet.clearMessage(12345); + expect(messageSet.getMessageAccessorOrNull(12345)).toBeNull(); + }); + + it('returns false for not present message', () => { + const messageSet = MessageSet.createEmpty(); + expect(messageSet.hasMessage(12345)).toBe(false); + }); + + it('returns true for present message', () => { + const messageSet = MessageSet.createEmpty(); + const message = TestMessage.createEmpty(); + messageSet.setMessage(12345, message); + expect(messageSet.hasMessage(12345)).toBe(true); + }); + + it('returns false for cleared message', () => { + const messageSet = MessageSet.createEmpty(); + const message = TestMessage.createEmpty(); + messageSet.setMessage(12345, message); + messageSet.clearMessage(12345); + expect(messageSet.hasMessage(12345)).toBe(false); + }); + + it('returns false for cleared message without it being present', () => { + const messageSet = MessageSet.createEmpty(); + messageSet.clearMessage(12345); + expect(messageSet.hasMessage(12345)).toBe(false); + }); + + const createMessageSet = () => { + const messageSet = MessageSet.createEmpty(); + const message = TestMessage.createEmpty(); + message.setInt32(1, 2); + messageSet.setMessage(12345, message); + + + const parsedKernel = + Kernel.fromArrayBuffer(messageSet.internalGetKernel().serialize()); + return MessageSet.fromKernel(parsedKernel); + }; + + it('pass through pivot for getMessageOrNull', () => { + const messageSet = createMessageSet(); + const message = + messageSet.getMessageOrNull(12345, TestMessage.instanceCreator, 2); + expect(message.internalGetKernel().getPivot()).toBe(2); + }); + + it('pass through pivot for getMessageAttach', () => { + const messageSet = createMessageSet(); + const message = + messageSet.getMessageAttach(12345, TestMessage.instanceCreator, 2); + expect(message.internalGetKernel().getPivot()).toBe(2); + }); + + it('pass through pivot for getMessageAccessorOrNull', () => { + const messageSet = createMessageSet(); + const kernel = messageSet.getMessageAccessorOrNull(12345, 2); + expect(kernel.getPivot()).toBe(2); + }); + + it('pick the last value in the stream', () => { + const arrayBuffer = createArrayBuffer( + 0x52, // Tag (field:10, length delimited) + 0x14, // Length of 20 bytes + 0x0B, // Start group fieldnumber 1 + 0x10, // Tag (field 2, varint) + 0xB9, // 12345 + 0x60, // 12345 + 0x1A, // Tag (field 3, length delimited) + 0x03, // length 3 + 0xA0, // Tag (fieldnumber 20, varint) + 0x01, // Tag (fieldnumber 20, varint) + 0x1E, // 30 + 0x0C, // Stop Group field number 1 + // second group + 0x0B, // Start group fieldnumber 1 + 0x10, // Tag (field 2, varint) + 0xB9, // 12345 + 0x60, // 12345 + 0x1A, // Tag (field 3, length delimited) + 0x03, // length 3 + 0xA0, // Tag (fieldnumber 20, varint) + 0x01, // Tag (fieldnumber 20, varint) + 0x01, // 1 + 0x0C // Stop Group field number 1 + ); + + const outerMessage = Kernel.fromArrayBuffer(arrayBuffer); + + const messageSet = outerMessage.getMessage(10, MessageSet.fromKernel); + + const message = + messageSet.getMessageOrNull(12345, TestMessage.instanceCreator); + expect(message.getInt32WithDefault(20)).toBe(1); + }); + + it('removes duplicates when read', () => { + const arrayBuffer = createArrayBuffer( + 0x52, // Tag (field:10, length delimited) + 0x14, // Length of 20 bytes + 0x0B, // Start group fieldnumber 1 + 0x10, // Tag (field 2, varint) + 0xB9, // 12345 + 0x60, // 12345 + 0x1A, // Tag (field 3, length delimited) + 0x03, // length 3 + 0xA0, // Tag (fieldnumber 20, varint) + 0x01, // Tag (fieldnumber 20, varint) + 0x1E, // 30 + 0x0C, // Stop Group field number 1 + // second group + 0x0B, // Start group fieldnumber 1 + 0x10, // Tag (field 2, varint) + 0xB9, // 12345 + 0x60, // 12345 + 0x1A, // Tag (field 3, length delimited) + 0x03, // length 3 + 0xA0, // Tag (fieldnumber 20, varint) + 0x01, // Tag (fieldnumber 20, varint) + 0x01, // 1 + 0x0C // Stop Group field number 1 + ); + + + const outerMessage = Kernel.fromArrayBuffer(arrayBuffer); + outerMessage.getMessageAttach(10, MessageSet.fromKernel); + + expect(outerMessage.serialize()) + .toEqual(createArrayBuffer( + 0x52, // Tag (field:10, length delimited) + 0x0A, // Length of 10 bytes + 0x0B, // Start group fieldnumber 1 + 0x10, // Tag (field 2, varint) + 0xB9, // 12345 + 0x60, // 12345 + 0x1A, // Tag (field 3, length delimited) + 0x03, // length 3 + 0xA0, // Tag (fieldnumber 20, varint) + 0x01, // Tag (fieldnumber 20, varint) + 0x01, // 1 + 0x0C // Stop Group field number 1 + )); + }); + + it('allow for large typeIds', () => { + const messageSet = MessageSet.createEmpty(); + const message = TestMessage.createEmpty(); + messageSet.setMessage(0xFFFFFFFE >>> 0, message); + expect(messageSet.hasMessage(0xFFFFFFFE >>> 0)).toBe(true); + }); +}); + +describe('Optional MessageSet does', () => { + // message Bar { + // optional MessageSet mset = 10; + //} + // + // message Foo { + // extend proto2.bridge.MessageSet { + // optional Foo message_set_extension = 12345; + // } + // optional int32 f20 = 20; + //} + + it('encode as a field', () => { + const fooMessage = Kernel.createEmpty(); + fooMessage.setInt32(20, 30); + + const messageSet = MessageSet.createEmpty(); + messageSet.setMessage(12345, TestMessage.instanceCreator(fooMessage)); + + const barMessage = Kernel.createEmpty(); + barMessage.setMessage(10, messageSet); + + expect(barMessage.serialize()) + .toEqual(createArrayBuffer( + 0x52, // Tag (field:10, length delimited) + 0x0A, // Length of 10 bytes + 0x0B, // Start group fieldnumber 1 + 0x10, // Tag (field 2, varint) + 0xB9, // 12345 + 0x60, // 12345 + 0x1A, // Tag (field 3, length delimited) + 0x03, // length 3 + 0xA0, // Tag (fieldnumber 20, varint) + 0x01, // Tag (fieldnumber 20, varint) + 0x1E, // 30 + 0x0C // Stop Group field number 1 + )); + }); + + it('deserializes', () => { + const fooMessage = Kernel.createEmpty(); + fooMessage.setInt32(20, 30); + + const messageSet = MessageSet.createEmpty(); + messageSet.setMessage(12345, TestMessage.instanceCreator(fooMessage)); + + + const barMessage = Kernel.createEmpty(); + barMessage.setMessage(10, messageSet); + + const arrayBuffer = barMessage.serialize(); + + const barMessageParsed = Kernel.fromArrayBuffer(arrayBuffer); + expect(barMessageParsed.hasFieldNumber(10)).toBe(true); + + const messageSetParsed = + barMessageParsed.getMessage(10, MessageSet.fromKernel); + + const fooMessageParsed = + messageSetParsed.getMessageOrNull(12345, TestMessage.instanceCreator) + .internalGetKernel(); + + expect(fooMessageParsed.getInt32WithDefault(20)).toBe(30); + }); +}); diff --git a/js/experimental/runtime/testing/binary/test_message.js b/js/experimental/runtime/testing/binary/test_message.js index a7aa8a1522..cfd264b324 100644 --- a/js/experimental/runtime/testing/binary/test_message.js +++ b/js/experimental/runtime/testing/binary/test_message.js @@ -13,6 +13,13 @@ const Kernel = goog.require('protobuf.runtime.Kernel'); * @implements {InternalMessage} */ class TestMessage { + /** + * @return {!TestMessage} + */ + static createEmpty() { + return TestMessage.instanceCreator(Kernel.createEmpty()); + } + /** * @param {!Kernel} kernel * @return {!TestMessage} @@ -31,7 +38,6 @@ class TestMessage { /** * @override - * @package * @return {!Kernel} */ internalGetKernel() { @@ -1760,4 +1766,4 @@ class TestMessage { } } -exports = TestMessage; \ No newline at end of file +exports = TestMessage;