diff --git a/packages/protobuf/src/index.ts b/packages/protobuf/src/index.ts index 5825d4ed67c..84458a6c2da 100644 --- a/packages/protobuf/src/index.ts +++ b/packages/protobuf/src/index.ts @@ -3,6 +3,7 @@ export * from './encode'; export * as Messages from './messages'; export * from './types'; export { parseConfigure, createMessageFromName, createMessageFromType } from './utils'; +export { loadDefinitions } from './load-definitions'; export * as MessagesSchema from './messages-schema'; // It's problem to reexport enums when they are under MessagesSchema namespace, check packages/connect/src/types/device.ts export { DeviceModelInternal } from './messages-schema'; diff --git a/packages/protobuf/src/load-definitions.ts b/packages/protobuf/src/load-definitions.ts new file mode 100644 index 00000000000..86c48a73dd9 --- /dev/null +++ b/packages/protobuf/src/load-definitions.ts @@ -0,0 +1,49 @@ +import { Root } from 'protobufjs/light'; + +type Definitions = Record; + +export const loadDefinitions = async ( + messages: Root, + packageName: string, + packageLoader: () => Definitions | Promise, +) => { + // check if package already exists + try { + const pkg = messages.lookup(packageName); + if (pkg) { + return; + } + } catch {} + + // get current MessageType enum + let enumType; + try { + enumType = messages.lookupEnum('MessageType'); + } catch {} + + // load definitions + const packageMessages = await packageLoader(); + const pkg = messages.define(packageName, packageMessages); + // get package MessageType enum + let packageEnumType; + try { + packageEnumType = pkg.lookupEnum('MessageType'); + } catch {} + + // merge MessageType enums + if (enumType && packageEnumType) { + try { + // move values from nested enum to top level + Object.keys(packageEnumType.values).forEach(key => { + enumType.add(key, packageEnumType.values[key]); + }); + // remove nested enum + pkg.remove(packageEnumType); + } catch (e) { + // remove whole package on merge error + messages.remove(pkg); + + throw e; + } + } +}; diff --git a/packages/protobuf/tests/load-definitions.test.ts b/packages/protobuf/tests/load-definitions.test.ts new file mode 100644 index 00000000000..ab61b93c40b --- /dev/null +++ b/packages/protobuf/tests/load-definitions.test.ts @@ -0,0 +1,100 @@ +import * as ProtoBuf from 'protobufjs/light'; + +import { loadDefinitions } from '../src/load-definitions'; + +describe('loadDefinitions', () => { + const createProtobufRoot = () => { + return ProtoBuf.Root.fromJSON({ + nested: { + MessageType: { + values: { + Initialize: 0, + }, + }, + }, + }); + }; + + it('merge MessageType enum', async () => { + const root = createProtobufRoot(); + await loadDefinitions(root, 'bitcoin', () => { + return Promise.resolve({ + MessageType: { + values: { + GetAddress: 29, + }, + }, + }); + }); + + const messageType = root.lookupEnum('MessageType')?.values; + expect(messageType).toEqual({ + Initialize: 0, + GetAddress: 29, + }); + }); + + it('throw on merge MessageType enum', async () => { + const root1 = createProtobufRoot(); + await expect( + loadDefinitions(root1, 'bitcoin', () => { + return Promise.resolve({ + MessageType: { + values: { + GetAddress: 0, + }, + }, + }); + }), + ).rejects.toThrow('duplicate id 0'); + expect(root1.lookup('bitcoin')).toBe(null); + + await expect( + loadDefinitions(createProtobufRoot(), 'bitcoin', () => { + return Promise.resolve({ + MessageType: { + values: { + Initialize: 1, + }, + }, + }); + }), + ).rejects.toThrow('duplicate name'); + }); + + it('create MessageType enum', async () => { + const root = createProtobufRoot(); + root.remove(root.lookupEnum('MessageType')); + + await loadDefinitions(root, 'bitcoin', () => { + return Promise.resolve({ + MessageType: { + values: { + GetAddress: 29, + }, + }, + }); + }); + + const messageType = root.lookupEnum('MessageType')?.values; + expect(messageType).toEqual({ + GetAddress: 29, + }); + }); + + it('already loaded', async () => { + const root = createProtobufRoot(); + root.define('bitcoin', { + MessageType: { + values: { + GetAddress: 29, + }, + }, + }); + + const spy = jest.fn(); + await loadDefinitions(root, 'bitcoin', spy); + + expect(spy).toHaveBeenCalledTimes(0); + }); +});