Skip to content

Commit e8dfc0a

Browse files
authored
feat: add support for maps (#75)
You can now use maps: ```protobuf message MapTypes { map<string, string> 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.
1 parent 26c569d commit e8dfc0a

File tree

8 files changed

+735
-29
lines changed

8 files changed

+735
-29
lines changed

.npmrc

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
; package-lock with tarball deps breaks lerna/nx - remove when https://github.com/semantic-release/github/pull/487 is merged
2+
package-lock=false

packages/protons/.aegir.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
build: {
44
config: {
55
platform: 'node'
6-
}
6+
},
7+
bundle: false
78
}
89
}

packages/protons/README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ It does have one or two differences:
6969
2. All 64 bit values are represented as `BigInt`s and not `Long`s (e.g. `int64`, `uint64`, `sint64` etc)
7070
3. Unset `optional` fields are set on the deserialized object forms as `undefined` instead of the default values
7171
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
72+
5. `map` fields can have keys of any type - protobufs.js [only supports strings](https://github.com/protobufjs/protobuf.js/issues/1203#issuecomment-488637338)
73+
6. `map` fields are deserialized as ES6 `Map`s - protobuf.js uses `Object`s
7274

7375
## Missing features
7476

75-
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`.
77+
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`.
7678

7779
## License
7880

packages/protons/src/index.ts

+103-26
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ function findDef (typeName: string, classDef: MessageDef, moduleDef: ModuleDef):
156156
function createDefaultObject (fields: Record<string, FieldDef>, messageDef: MessageDef, moduleDef: ModuleDef): string {
157157
const output = Object.entries(fields)
158158
.map(([name, fieldDef]) => {
159+
if (fieldDef.map) {
160+
return `${name}: new Map<${types[fieldDef.keyType ?? 'string']}, ${types[fieldDef.valueType]}>()`
161+
}
162+
159163
if (fieldDef.repeated) {
160164
return `${name}: []`
161165
}
@@ -280,10 +284,17 @@ interface FieldDef {
280284
repeated: boolean
281285
message: boolean
282286
enum: boolean
287+
map: boolean
288+
valueType: string
289+
keyType: string
283290
}
284291

285292
function defineFields (fields: Record<string, FieldDef>, messageDef: MessageDef, moduleDef: ModuleDef): string[] {
286293
return Object.entries(fields).map(([fieldName, fieldDef]) => {
294+
if (fieldDef.map) {
295+
return `${fieldName}: Map<${findTypeName(fieldDef.keyType ?? 'string', messageDef, moduleDef)}, ${findTypeName(fieldDef.valueType, messageDef, moduleDef)}>`
296+
}
297+
287298
return `${fieldName}${fieldDef.optional ? '?' : ''}: ${findTypeName(fieldDef.type, messageDef, moduleDef)}${fieldDef.repeated ? '[]' : ''}`
288299
})
289300
}
@@ -365,7 +376,7 @@ export interface ${messageDef.name} {
365376
${Object.entries(fields)
366377
.map(([name, fieldDef]) => {
367378
let codec: string = encoders[fieldDef.type]
368-
let type: string = fieldDef.type
379+
let type: string = fieldDef.map ? 'message' : fieldDef.type
369380
let typeName: string = ''
370381
371382
if (codec == null) {
@@ -383,8 +394,10 @@ ${Object.entries(fields)
383394
384395
let valueTest = `obj.${name} != null`
385396
386-
// proto3 singular fields should only be written out if they are not the default value
387-
if (!fieldDef.optional && !fieldDef.repeated) {
397+
if (fieldDef.map) {
398+
valueTest = `obj.${name} != null && obj.${name}.size !== 0`
399+
} else if (!fieldDef.optional && !fieldDef.repeated) {
400+
// proto3 singular fields should only be written out if they are not the default value
388401
if (defaultValueTestGenerators[type] != null) {
389402
valueTest = `opts.writeDefaults === true || ${defaultValueTestGenerators[type](`obj.${name}`)}`
390403
} else if (type === 'enum') {
@@ -413,10 +426,11 @@ ${Object.entries(fields)
413426
let writeField = createWriteField(`obj.${name}`)
414427
415428
if (fieldDef.repeated) {
416-
writeField = `
417-
for (const value of obj.${name}) {
429+
if (fieldDef.map) {
430+
writeField = `
431+
for (const [key, value] of obj.${name}.entries()) {
418432
${
419-
createWriteField('value')
433+
createWriteField('{ key, value }')
420434
.split('\n')
421435
.map(s => {
422436
const trimmed = s.trim()
@@ -425,8 +439,24 @@ ${Object.entries(fields)
425439
})
426440
.join('\n')
427441
}
442+
}
443+
`.trim()
444+
} else {
445+
writeField = `
446+
for (const value of obj.${name}) {
447+
${
448+
createWriteField('value')
449+
.split('\n')
450+
.map(s => {
451+
const trimmed = s.trim()
452+
453+
return trimmed === '' ? trimmed : ` ${s}`
454+
})
455+
.join('\n')
456+
}
428457
}
429458
`.trim()
459+
}
430460
}
431461
432462
return `
@@ -448,30 +478,46 @@ ${Object.entries(fields)
448478
449479
switch (tag >>> 3) {
450480
${Object.entries(fields)
451-
.map(([name, fieldDef]) => {
452-
let codec: string = encoders[fieldDef.type]
453-
let type: string = fieldDef.type
454-
455-
if (codec == null) {
456-
if (fieldDef.enum) {
457-
moduleDef.imports.add('enumeration')
458-
type = 'enum'
459-
} else {
460-
moduleDef.imports.add('message')
461-
type = 'message'
481+
.map(([fieldName, fieldDef]) => {
482+
function createReadField (fieldName: string, fieldDef: FieldDef): string {
483+
let codec: string = encoders[fieldDef.type]
484+
let type: string = fieldDef.type
485+
486+
if (codec == null) {
487+
if (fieldDef.enum) {
488+
moduleDef.imports.add('enumeration')
489+
type = 'enum'
490+
} else {
491+
moduleDef.imports.add('message')
492+
type = 'message'
493+
}
494+
495+
const typeName = findTypeName(fieldDef.type, messageDef, moduleDef)
496+
codec = `${typeName}.codec()`
462497
}
463498
464-
const typeName = findTypeName(fieldDef.type, messageDef, moduleDef)
465-
codec = `${typeName}.codec()`
466-
}
499+
const parseValue = `${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()}`
500+
501+
if (fieldDef.map) {
502+
return `case ${fieldDef.id}: {
503+
const entry = ${parseValue}
504+
obj.${fieldName}.set(entry.key, entry.value)
505+
break
506+
}`
507+
} else if (fieldDef.repeated) {
508+
return `case ${fieldDef.id}:
509+
obj.${fieldName}.push(${parseValue})
510+
break`
511+
}
467512
468-
return `case ${fieldDef.id}:${fieldDef.rule === 'repeated'
469-
? `
470-
obj.${name}.push(${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()})`
471-
: `
472-
obj.${name} = ${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()}`}
513+
return `case ${fieldDef.id}:
514+
obj.${fieldName} = ${parseValue}
473515
break`
474-
}).join('\n ')}
516+
}
517+
518+
return createReadField(fieldName, fieldDef)
519+
})
520+
.join('\n ')}
475521
default:
476522
reader.skipType(tag & 7)
477523
break
@@ -543,6 +589,7 @@ function defineModule (def: ClassDef): ModuleDef {
543589
const fieldDef = classDef.fields[name]
544590
fieldDef.repeated = fieldDef.rule === 'repeated'
545591
fieldDef.optional = !fieldDef.repeated && fieldDef.options?.proto3_optional === true
592+
fieldDef.map = fieldDef.keyType != null
546593
}
547594
}
548595

@@ -598,6 +645,36 @@ export async function generate (source: string, flags: Flags): Promise<void> {
598645
}
599646

600647
const def = JSON.parse(json)
648+
649+
for (const [className, classDef] of Object.entries<any>(def.nested)) {
650+
for (const [fieldName, fieldDef] of Object.entries<any>(classDef.fields ?? {})) {
651+
if (fieldDef.keyType == null) {
652+
continue
653+
}
654+
655+
// https://developers.google.com/protocol-buffers/docs/proto3#backwards_compatibility
656+
const mapEntryType = `${className}$${fieldName}Entry`
657+
658+
classDef.nested = classDef.nested ?? {}
659+
classDef.nested[mapEntryType] = {
660+
fields: {
661+
key: {
662+
type: fieldDef.keyType,
663+
id: 1
664+
},
665+
value: {
666+
type: fieldDef.type,
667+
id: 2
668+
}
669+
}
670+
}
671+
672+
fieldDef.valueType = fieldDef.type
673+
fieldDef.type = mapEntryType
674+
fieldDef.rule = 'repeated'
675+
}
676+
}
677+
601678
const moduleDef = defineModule(def)
602679

603680
let lines = [
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
syntax = "proto3";
2+
3+
message SubMessage {
4+
string foo = 1;
5+
}
6+
7+
message MapTypes {
8+
map<string, string> stringMap = 1;
9+
map<int32, int32> intMap = 2;
10+
map<bool, bool> boolMap = 3;
11+
map<string, SubMessage> messageMap = 4;
12+
}

0 commit comments

Comments
 (0)