From e8dfc0a6a7700225e2cf9c6d2e82d17979da1549 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 12 Jan 2023 16:56:19 +0000 Subject: [PATCH] feat: add support for maps (#75) You can now use maps: ```protobuf message MapTypes { map stringMap = 1; } ``` They are deserlialized as ES6 `Map`s and can support keys of any type - n.b. protobuf.js deserializes maps as `Object`s and only supports round tripping string keys. --- .npmrc | 2 + packages/protons/.aegir.js | 3 +- packages/protons/README.md | 4 +- packages/protons/src/index.ts | 129 ++++-- packages/protons/test/fixtures/maps.proto | 12 + packages/protons/test/fixtures/maps.ts | 452 ++++++++++++++++++++++ packages/protons/test/index.spec.ts | 14 +- packages/protons/test/maps.spec.ts | 148 +++++++ 8 files changed, 735 insertions(+), 29 deletions(-) create mode 100644 .npmrc create mode 100644 packages/protons/test/fixtures/maps.proto create mode 100644 packages/protons/test/fixtures/maps.ts create mode 100644 packages/protons/test/maps.spec.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c5ebf5e --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +; package-lock with tarball deps breaks lerna/nx - remove when https://github.com/semantic-release/github/pull/487 is merged +package-lock=false diff --git a/packages/protons/.aegir.js b/packages/protons/.aegir.js index 135a6a2..5a23c24 100644 --- a/packages/protons/.aegir.js +++ b/packages/protons/.aegir.js @@ -3,6 +3,7 @@ export default { build: { config: { platform: 'node' - } + }, + bundle: false } } diff --git a/packages/protons/README.md b/packages/protons/README.md index b8ccd17..ea435bb 100644 --- a/packages/protons/README.md +++ b/packages/protons/README.md @@ -69,10 +69,12 @@ It does have one or two differences: 2. All 64 bit values are represented as `BigInt`s and not `Long`s (e.g. `int64`, `uint64`, `sint64` etc) 3. Unset `optional` fields are set on the deserialized object forms as `undefined` instead of the default values 4. `singular` fields set to default values are not serialized and are set to default values when deserialized if not set - protobuf.js [diverges from the language guide](https://github.com/protobufjs/protobuf.js/issues/1468#issuecomment-745177012) around this feature +5. `map` fields can have keys of any type - protobufs.js [only supports strings](https://github.com/protobufjs/protobuf.js/issues/1203#issuecomment-488637338) +6. `map` fields are deserialized as ES6 `Map`s - protobuf.js uses `Object`s ## Missing features -Some features are missing `OneOf`, `Map`s, etc due to them not being needed so far in ipfs/libp2p. If these features are important to you, please open PRs implementing them along with tests comparing the generated bytes to `protobuf.js` and `pbjs`. +Some features are missing `OneOf`s, etc due to them not being needed so far in ipfs/libp2p. If these features are important to you, please open PRs implementing them along with tests comparing the generated bytes to `protobuf.js` and `pbjs`. ## License diff --git a/packages/protons/src/index.ts b/packages/protons/src/index.ts index 246bd95..3ae6789 100644 --- a/packages/protons/src/index.ts +++ b/packages/protons/src/index.ts @@ -156,6 +156,10 @@ function findDef (typeName: string, classDef: MessageDef, moduleDef: ModuleDef): function createDefaultObject (fields: Record, messageDef: MessageDef, moduleDef: ModuleDef): string { const output = Object.entries(fields) .map(([name, fieldDef]) => { + if (fieldDef.map) { + return `${name}: new Map<${types[fieldDef.keyType ?? 'string']}, ${types[fieldDef.valueType]}>()` + } + if (fieldDef.repeated) { return `${name}: []` } @@ -280,10 +284,17 @@ interface FieldDef { repeated: boolean message: boolean enum: boolean + map: boolean + valueType: string + keyType: string } function defineFields (fields: Record, messageDef: MessageDef, moduleDef: ModuleDef): string[] { return Object.entries(fields).map(([fieldName, fieldDef]) => { + if (fieldDef.map) { + return `${fieldName}: Map<${findTypeName(fieldDef.keyType ?? 'string', messageDef, moduleDef)}, ${findTypeName(fieldDef.valueType, messageDef, moduleDef)}>` + } + return `${fieldName}${fieldDef.optional ? '?' : ''}: ${findTypeName(fieldDef.type, messageDef, moduleDef)}${fieldDef.repeated ? '[]' : ''}` }) } @@ -365,7 +376,7 @@ export interface ${messageDef.name} { ${Object.entries(fields) .map(([name, fieldDef]) => { let codec: string = encoders[fieldDef.type] - let type: string = fieldDef.type + let type: string = fieldDef.map ? 'message' : fieldDef.type let typeName: string = '' if (codec == null) { @@ -383,8 +394,10 @@ ${Object.entries(fields) let valueTest = `obj.${name} != null` - // proto3 singular fields should only be written out if they are not the default value - if (!fieldDef.optional && !fieldDef.repeated) { + if (fieldDef.map) { + valueTest = `obj.${name} != null && obj.${name}.size !== 0` + } else if (!fieldDef.optional && !fieldDef.repeated) { + // proto3 singular fields should only be written out if they are not the default value if (defaultValueTestGenerators[type] != null) { valueTest = `opts.writeDefaults === true || ${defaultValueTestGenerators[type](`obj.${name}`)}` } else if (type === 'enum') { @@ -413,10 +426,11 @@ ${Object.entries(fields) let writeField = createWriteField(`obj.${name}`) if (fieldDef.repeated) { - writeField = ` - for (const value of obj.${name}) { + if (fieldDef.map) { + writeField = ` + for (const [key, value] of obj.${name}.entries()) { ${ - createWriteField('value') + createWriteField('{ key, value }') .split('\n') .map(s => { const trimmed = s.trim() @@ -425,8 +439,24 @@ ${Object.entries(fields) }) .join('\n') } + } + `.trim() + } else { + writeField = ` + for (const value of obj.${name}) { + ${ + createWriteField('value') + .split('\n') + .map(s => { + const trimmed = s.trim() + + return trimmed === '' ? trimmed : ` ${s}` + }) + .join('\n') + } } `.trim() + } } return ` @@ -448,30 +478,46 @@ ${Object.entries(fields) switch (tag >>> 3) { ${Object.entries(fields) - .map(([name, fieldDef]) => { - let codec: string = encoders[fieldDef.type] - let type: string = fieldDef.type - - if (codec == null) { - if (fieldDef.enum) { - moduleDef.imports.add('enumeration') - type = 'enum' - } else { - moduleDef.imports.add('message') - type = 'message' + .map(([fieldName, fieldDef]) => { + function createReadField (fieldName: string, fieldDef: FieldDef): string { + let codec: string = encoders[fieldDef.type] + let type: string = fieldDef.type + + if (codec == null) { + if (fieldDef.enum) { + moduleDef.imports.add('enumeration') + type = 'enum' + } else { + moduleDef.imports.add('message') + type = 'message' + } + + const typeName = findTypeName(fieldDef.type, messageDef, moduleDef) + codec = `${typeName}.codec()` } - const typeName = findTypeName(fieldDef.type, messageDef, moduleDef) - codec = `${typeName}.codec()` - } + const parseValue = `${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()}` + + if (fieldDef.map) { + return `case ${fieldDef.id}: { + const entry = ${parseValue} + obj.${fieldName}.set(entry.key, entry.value) + break + }` + } else if (fieldDef.repeated) { + return `case ${fieldDef.id}: + obj.${fieldName}.push(${parseValue}) + break` + } - return `case ${fieldDef.id}:${fieldDef.rule === 'repeated' -? ` - obj.${name}.push(${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()})` -: ` - obj.${name} = ${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()}`} + return `case ${fieldDef.id}: + obj.${fieldName} = ${parseValue} break` - }).join('\n ')} + } + + return createReadField(fieldName, fieldDef) + }) + .join('\n ')} default: reader.skipType(tag & 7) break @@ -543,6 +589,7 @@ function defineModule (def: ClassDef): ModuleDef { const fieldDef = classDef.fields[name] fieldDef.repeated = fieldDef.rule === 'repeated' fieldDef.optional = !fieldDef.repeated && fieldDef.options?.proto3_optional === true + fieldDef.map = fieldDef.keyType != null } } @@ -598,6 +645,36 @@ export async function generate (source: string, flags: Flags): Promise { } const def = JSON.parse(json) + + for (const [className, classDef] of Object.entries(def.nested)) { + for (const [fieldName, fieldDef] of Object.entries(classDef.fields ?? {})) { + if (fieldDef.keyType == null) { + continue + } + + // https://developers.google.com/protocol-buffers/docs/proto3#backwards_compatibility + const mapEntryType = `${className}$${fieldName}Entry` + + classDef.nested = classDef.nested ?? {} + classDef.nested[mapEntryType] = { + fields: { + key: { + type: fieldDef.keyType, + id: 1 + }, + value: { + type: fieldDef.type, + id: 2 + } + } + } + + fieldDef.valueType = fieldDef.type + fieldDef.type = mapEntryType + fieldDef.rule = 'repeated' + } + } + const moduleDef = defineModule(def) let lines = [ diff --git a/packages/protons/test/fixtures/maps.proto b/packages/protons/test/fixtures/maps.proto new file mode 100644 index 0000000..cca3338 --- /dev/null +++ b/packages/protons/test/fixtures/maps.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +message SubMessage { + string foo = 1; +} + +message MapTypes { + map stringMap = 1; + map intMap = 2; + map boolMap = 3; + map messageMap = 4; +} diff --git a/packages/protons/test/fixtures/maps.ts b/packages/protons/test/fixtures/maps.ts new file mode 100644 index 0000000..aab9f8b --- /dev/null +++ b/packages/protons/test/fixtures/maps.ts @@ -0,0 +1,452 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { Codec } from 'protons-runtime' + +export interface SubMessage { + foo: string +} + +export namespace SubMessage { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.foo !== '') { + w.uint32(10) + w.string(obj.foo) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + foo: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.foo = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: SubMessage): Uint8Array => { + return encodeMessage(obj, SubMessage.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): SubMessage => { + return decodeMessage(buf, SubMessage.codec()) + } +} + +export interface MapTypes { + stringMap: Map + intMap: Map + boolMap: Map + messageMap: Map +} + +export namespace MapTypes { + export interface MapTypes$stringMapEntry { + key: string + value: string + } + + export namespace MapTypes$stringMapEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.key !== '') { + w.uint32(10) + w.string(obj.key) + } + + if (opts.writeDefaults === true || obj.value !== '') { + w.uint32(18) + w.string(obj.value) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: MapTypes$stringMapEntry): Uint8Array => { + return encodeMessage(obj, MapTypes$stringMapEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): MapTypes$stringMapEntry => { + return decodeMessage(buf, MapTypes$stringMapEntry.codec()) + } + } + + export interface MapTypes$intMapEntry { + key: number + value: number + } + + export namespace MapTypes$intMapEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.key !== 0) { + w.uint32(8) + w.int32(obj.key) + } + + if (opts.writeDefaults === true || obj.value !== 0) { + w.uint32(16) + w.int32(obj.value) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: 0, + value: 0 + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.int32() + break + case 2: + obj.value = reader.int32() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: MapTypes$intMapEntry): Uint8Array => { + return encodeMessage(obj, MapTypes$intMapEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): MapTypes$intMapEntry => { + return decodeMessage(buf, MapTypes$intMapEntry.codec()) + } + } + + export interface MapTypes$boolMapEntry { + key: boolean + value: boolean + } + + export namespace MapTypes$boolMapEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.key !== false) { + w.uint32(8) + w.bool(obj.key) + } + + if (opts.writeDefaults === true || obj.value !== false) { + w.uint32(16) + w.bool(obj.value) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: false, + value: false + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.bool() + break + case 2: + obj.value = reader.bool() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: MapTypes$boolMapEntry): Uint8Array => { + return encodeMessage(obj, MapTypes$boolMapEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): MapTypes$boolMapEntry => { + return decodeMessage(buf, MapTypes$boolMapEntry.codec()) + } + } + + export interface MapTypes$messageMapEntry { + key: string + value: SubMessage + } + + export namespace MapTypes$messageMapEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.key !== '') { + w.uint32(10) + w.string(obj.key) + } + + if (obj.value != null) { + w.uint32(18) + SubMessage.codec().encode(obj.value, w, { + writeDefaults: false + }) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: undefined + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = SubMessage.codec().decode(reader, reader.uint32()) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: MapTypes$messageMapEntry): Uint8Array => { + return encodeMessage(obj, MapTypes$messageMapEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): MapTypes$messageMapEntry => { + return decodeMessage(buf, MapTypes$messageMapEntry.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.stringMap != null && obj.stringMap.size !== 0) { + for (const [key, value] of obj.stringMap.entries()) { + w.uint32(10) + MapTypes.MapTypes$stringMapEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + + if (obj.intMap != null && obj.intMap.size !== 0) { + for (const [key, value] of obj.intMap.entries()) { + w.uint32(18) + MapTypes.MapTypes$intMapEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + + if (obj.boolMap != null && obj.boolMap.size !== 0) { + for (const [key, value] of obj.boolMap.entries()) { + w.uint32(26) + MapTypes.MapTypes$boolMapEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + + if (obj.messageMap != null && obj.messageMap.size !== 0) { + for (const [key, value] of obj.messageMap.entries()) { + w.uint32(34) + MapTypes.MapTypes$messageMapEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + stringMap: new Map(), + intMap: new Map(), + boolMap: new Map(), + messageMap: new Map() + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: { + const entry = MapTypes.MapTypes$stringMapEntry.codec().decode(reader, reader.uint32()) + obj.stringMap.set(entry.key, entry.value) + break + } + case 2: { + const entry = MapTypes.MapTypes$intMapEntry.codec().decode(reader, reader.uint32()) + obj.intMap.set(entry.key, entry.value) + break + } + case 3: { + const entry = MapTypes.MapTypes$boolMapEntry.codec().decode(reader, reader.uint32()) + obj.boolMap.set(entry.key, entry.value) + break + } + case 4: { + const entry = MapTypes.MapTypes$messageMapEntry.codec().decode(reader, reader.uint32()) + obj.messageMap.set(entry.key, entry.value) + break + } + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: MapTypes): Uint8Array => { + return encodeMessage(obj, MapTypes.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): MapTypes => { + return decodeMessage(buf, MapTypes.codec()) + } +} diff --git a/packages/protons/test/index.spec.ts b/packages/protons/test/index.spec.ts index b9ac8d4..08db22b 100644 --- a/packages/protons/test/index.spec.ts +++ b/packages/protons/test/index.spec.ts @@ -67,13 +67,18 @@ function normalizePbjs (obj: any, target: any): any { if (Array.isArray(target[key]) && output[key] == null) { output[key] = [] } + + // pbjs does not set maps by default + if (target[key] instanceof Map) { + output[key] = new Map() + } } return output } /** - * Paper over differences between protons and pbjs output + * Paper over differences between protons and protobuf.js output */ function normalizeProtonbufjs (obj: any, target: any): any { let output = bigintifyLongs(obj) @@ -86,6 +91,13 @@ function normalizeProtonbufjs (obj: any, target: any): any { } } + for (const key of Object.keys(target)) { + // protobujs uses plain objects instead of maps + if (target[key] instanceof Map) { + output[key] = new Map(Object.entries(output[key])) + } + } + return output } diff --git a/packages/protons/test/maps.spec.ts b/packages/protons/test/maps.spec.ts new file mode 100644 index 0000000..dc1c58b --- /dev/null +++ b/packages/protons/test/maps.spec.ts @@ -0,0 +1,148 @@ +/* eslint-env mocha */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + +import { expect } from 'aegir/chai' +import { MapTypes, SubMessage } from './fixtures/maps.js' +import protobufjs from 'protobufjs' +import Long from 'long' + +function longifyBigInts (obj: any): any { + const output = { + ...obj + } + + for (const key of Object.keys(output)) { + if (typeof output[key] === 'bigint') { + output[key] = Long.fromString(`${output[key].toString()}`) + } + } + + return output +} + +function bigintifyLongs (obj: any): any { + const output = { + ...obj + } + + for (const key of Object.keys(output)) { + if (output[key]?.low != null && output[key]?.high != null) { + output[key] = BigInt(new Long(output[key].low, output[key].high, output[key].unsigned).toString()) + } + } + + return output +} + +function uint8ArrayifyBytes (obj: any): any { + const output = { + ...obj + } + + for (const key of Object.keys(output)) { + if (output[key] instanceof Uint8Array) { + output[key] = Uint8Array.from(output[key]) + } + } + + return output +} + +function objectifyMaps (obj: any): any { + const output = { + ...obj + } + + for (const key of Object.keys(output)) { + if (output[key] instanceof Map) { + const obj: Record = {} + const entries: Array<[key: any, value: any]> = output[key].entries() + + for (const [key, value] of entries) { + obj[key] = value + } + + output[key] = obj + } + } + + return output +} + +/** + * Paper over differences between protons and protobuf.js output + */ +function normalizeProtonbufjs (obj: any, target: any): any { + let output = bigintifyLongs(obj) + output = uint8ArrayifyBytes(output) + + for (const key of Object.keys(output)) { + // protobujs sets unset message fields to `null`, protons does not set the field at all + if (output[key] === null && target[key] == null) { + delete output[key] // eslint-disable-line @typescript-eslint/no-dynamic-delete + } + } + + for (const key of Object.keys(target)) { + // protobujs uses plain objects instead of maps + if (target[key] instanceof Map) { + output[key] = new Map(Object.entries(output[key] ?? {})) + } + } + + return output +} + +interface TestEncodingOptions { + compareBytes?: boolean + comparePbjs?: boolean +} + +/** + * Ensure: + * + * 1. the generated bytes between protons, pbjs and protobuf.js are the same + * 2. protons and protobuf.js agree on deserialization + */ +function testEncodings (obj: any, protons: any, proto: string, typeName: string, opts: TestEncodingOptions = {}): void { + const protobufJsSchema = protobufjs.loadSync(proto).lookupType(typeName) + const protobufJsBuf = protobufJsSchema.encode(protobufJsSchema.fromObject(objectifyMaps(longifyBigInts(obj)))).finish() + + const encoded = protons.encode(obj) + + if (opts.compareBytes !== false) { + expect(encoded).to.equalBytes(protobufJsBuf) + } + + expect(protons.decode(encoded)).to.deep.equal(obj) + expect(protons.decode(protobufJsBuf)).to.deep.equal(obj) + + expect(normalizeProtonbufjs(protobufJsSchema.toObject(protobufJsSchema.decode(encoded), { + enums: String, + defaults: true + }), obj)).to.deep.equal(obj) +} + +describe('maps', () => { + it('should encode all the types', () => { + const obj: MapTypes = { + stringMap: new Map(), + intMap: new Map(), + boolMap: new Map(), + messageMap: new Map() + } + + testEncodings(obj, MapTypes, './test/fixtures/maps.proto', 'MapTypes') + }) + + it('should encode all the types with values', () => { + const obj: MapTypes = { + stringMap: new Map([['key', 'value']]), + intMap: new Map(), // protobuf.js only supports strings as keys + boolMap: new Map(), // protobuf.js only supports strings as keys + messageMap: new Map([['key', { foo: 'bar' }]]) + } + + testEncodings(obj, MapTypes, './test/fixtures/maps.proto', 'MapTypes') + }) +})