Skip to content

Commit 4324178

Browse files
committed
fix: singular fields should be optional to write
As per [`proto3`'s language guide](https://developers.google.com/protocol-buffers/docs/proto3#specifying_field_rules): > Message fields can be one of the following: > - singular: a well-formed message can have **zero** or one of this field (but not more than one). And this is the default field rule for proto3 syntax. This means that all `proto3` fields are effectively optional when writing, so it's relatively easy to implement by accepting a `Partial` version of the message interface for encoding. When reading messages singular fields are initialized to their default values when reading and optional values are not so the plain non-`Partial` interface can be returned. This also has the nice side effect of not requring the user to pass empty lists/maps for `repeated` and `map` fields which never really felt right. These values are initted to their empty forms when reading messages from the wire. Fixes #42
1 parent 76aa198 commit 4324178

File tree

16 files changed

+177
-133
lines changed

16 files changed

+177
-133
lines changed

packages/protons-benchmark/src/protons/rpc.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export namespace RPC {
115115

116116
if (opts.writeDefaults === true || obj.topic !== '') {
117117
w.uint32(34)
118-
w.string(obj.topic)
118+
w.string(obj.topic ?? '')
119119
}
120120

121121
if (obj.signature != null) {

packages/protons-runtime/src/codec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface EncodeOptions {
1616
}
1717

1818
export interface EncodeFunction<T> {
19-
(value: T, writer: Writer, opts?: EncodeOptions): void
19+
(value: Partial<T>, writer: Writer, opts?: EncodeOptions): void
2020
}
2121

2222
export interface DecodeFunction<T> {

packages/protons-runtime/src/codecs/message.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export interface Factory<A, T> {
66
new (obj: A): T
77
}
88

9-
export function message <T> (encode: (obj: T, writer: Writer, opts?: EncodeOptions) => void, decode: (reader: Reader, length?: number) => T): Codec<T> {
9+
export function message <T> (encode: (obj: Partial<T>, writer: Writer, opts?: EncodeOptions) => void, decode: (reader: Reader, length?: number) => T): Codec<T> {
1010
return createCodec('message', CODEC_TYPES.LENGTH_DELIMITED, encode, decode)
1111
}

packages/protons/src/index.ts

+37-26
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,22 @@ const types: Record<string, string> = {
3737
uint64: 'bigint'
3838
}
3939

40-
const encoderGenerators: Record<string, (val: string) => string> = {
41-
bool: (val) => `w.bool(${val})`,
42-
bytes: (val) => `w.bytes(${val})`,
43-
double: (val) => `w.double(${val})`,
44-
fixed32: (val) => `w.fixed32(${val})`,
45-
fixed64: (val) => `w.fixed64(${val})`,
46-
float: (val) => `w.float(${val})`,
47-
int32: (val) => `w.int32(${val})`,
48-
int64: (val) => `w.int64(${val})`,
49-
sfixed32: (val) => `w.sfixed32(${val})`,
50-
sfixed64: (val) => `w.sfixed64(${val})`,
51-
sint32: (val) => `w.sint32(${val})`,
52-
sint64: (val) => `w.sint64(${val})`,
53-
string: (val) => `w.string(${val})`,
54-
uint32: (val) => `w.uint32(${val})`,
55-
uint64: (val) => `w.uint64(${val})`
40+
const encoderGenerators: Record<string, (val: string, includeDefault: boolean) => string> = {
41+
bool: (val, includeDefault) => `w.bool(${val}${includeDefault ? ' ?? false' : '' })`,
42+
bytes: (val, includeDefault) => `w.bytes(${val}${includeDefault ? ' ?? new Uint8Array(0)' : '' })`,
43+
double: (val, includeDefault) => `w.double(${val}${includeDefault ? ' ?? 0' : '' })`,
44+
fixed32: (val, includeDefault) => `w.fixed32(${val}${includeDefault ? ' ?? 0' : '' })`,
45+
fixed64: (val, includeDefault) => `w.fixed64(${val}${includeDefault ? ' ?? 0n' : '' })`,
46+
float: (val, includeDefault) => `w.float(${val}${includeDefault ? ' ?? 0' : '' })`,
47+
int32: (val, includeDefault) => `w.int32(${val}${includeDefault ? ' ?? 0' : '' })`,
48+
int64: (val, includeDefault) => `w.int64(${val}${includeDefault ? ' ?? 0n' : '' })`,
49+
sfixed32: (val, includeDefault) => `w.sfixed32(${val}${includeDefault ? ' ?? 0' : '' })`,
50+
sfixed64: (val, includeDefault) => `w.sfixed64(${val}${includeDefault ? ' ?? 0n' : '' })`,
51+
sint32: (val, includeDefault) => `w.sint32(${val}${includeDefault ? ' ?? 0' : '' })`,
52+
sint64: (val, includeDefault) => `w.sint64(${val}${includeDefault ? ' ?? 0n' : '' })`,
53+
string: (val, includeDefault) => `w.string(${val}${includeDefault ? ' ?? \'\'' : '' })`,
54+
uint32: (val, includeDefault) => `w.uint32(${val}${includeDefault ? ' ?? 0' : '' })`,
55+
uint64: (val, includeDefault) => `w.uint64(${val}${includeDefault ? ' ?? 0n' : '' })`
5656
}
5757

5858
const decoderGenerators: Record<string, () => string> = {
@@ -401,15 +401,26 @@ export interface ${messageDef.name} {
401401
}
402402
}
403403

404-
function createWriteField (valueVar: string): string {
404+
function createWriteField (valueVar: string): (includeDefault: boolean ) => string {
405405
const id = (fieldDef.id << 3) | codecTypes[type]
406+
let defaultValue = ''
406407

407-
let writeField = `w.uint32(${id})
408-
${encoderGenerators[type] == null ? `${codec}.encode(${valueVar}, w)` : encoderGenerators[type](valueVar)}`
408+
if (fieldDef.enum) {
409+
const def = findDef(fieldDef.type, messageDef, moduleDef)
410+
411+
if (!isEnumDef(def)) {
412+
throw new Error(`${fieldDef.type} was not enum def`)
413+
}
414+
415+
defaultValue = Object.keys(def.values)[0]
416+
}
417+
418+
let writeField = (includeDefault: boolean) => `w.uint32(${id})
419+
${encoderGenerators[type] == null ? `${codec}.encode(${valueVar}${includeDefault ? ` ?? ${typeName}.${defaultValue}` : ''}, w)` : encoderGenerators[type](valueVar, includeDefault)}`
409420

410421
if (type === 'message') {
411422
// message fields are only written if they have values
412-
writeField = `w.uint32(${id})
423+
writeField = () => `w.uint32(${id})
413424
${typeName}.codec().encode(${valueVar}, w, {
414425
writeDefaults: ${Boolean(fieldDef.repeated).toString()}
415426
})`
@@ -422,10 +433,10 @@ export interface ${messageDef.name} {
422433

423434
if (fieldDef.repeated) {
424435
if (fieldDef.map) {
425-
writeField = `
436+
writeField = () => `
426437
for (const [key, value] of obj.${name}.entries()) {
427438
${
428-
createWriteField('{ key, value }')
439+
createWriteField('{ key, value }')(false)
429440
.split('\n')
430441
.map(s => {
431442
const trimmed = s.trim()
@@ -437,10 +448,10 @@ export interface ${messageDef.name} {
437448
}
438449
`.trim()
439450
} else {
440-
writeField = `
451+
writeField = () => `
441452
for (const value of obj.${name}) {
442453
${
443-
createWriteField('value')
454+
createWriteField('value')(false)
444455
.split('\n')
445456
.map(s => {
446457
const trimmed = s.trim()
@@ -456,7 +467,7 @@ export interface ${messageDef.name} {
456467

457468
return `
458469
if (${valueTest}) {
459-
${writeField}
470+
${writeField(valueTest.includes('opts.writeDefaults === true'))}
460471
}`
461472
}).join('\n')
462473

@@ -537,7 +548,7 @@ ${encodeFields === '' ? '' : `${encodeFields}\n`}
537548
return _codec
538549
}
539550
540-
export const encode = (obj: ${messageDef.name}): Uint8Array => {
551+
export const encode = (obj: Partial<${messageDef.name}>): Uint8Array => {
541552
return encodeMessage(obj, ${messageDef.name}.codec())
542553
}
543554

packages/protons/test/fixtures/basic.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export namespace Basic {
3030

3131
if (opts.writeDefaults === true || obj.num !== 0) {
3232
w.uint32(16)
33-
w.int32(obj.num)
33+
w.int32(obj.num ?? 0)
3434
}
3535

3636
if (opts.lengthDelimited !== false) {
@@ -66,7 +66,7 @@ export namespace Basic {
6666
return _codec
6767
}
6868

69-
export const encode = (obj: Basic): Uint8Array => {
69+
export const encode = (obj: Partial<Basic>): Uint8Array => {
7070
return encodeMessage(obj, Basic.codec())
7171
}
7272

@@ -112,7 +112,7 @@ export namespace Empty {
112112
return _codec
113113
}
114114

115-
export const encode = (obj: Empty): Uint8Array => {
115+
export const encode = (obj: Partial<Empty>): Uint8Array => {
116116
return encodeMessage(obj, Empty.codec())
117117
}
118118

packages/protons/test/fixtures/circuit.proto

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ message CircuitRelay {
2929
}
3030

3131
message Peer {
32-
required bytes id = 1; // peer id
32+
bytes id = 1; // peer id
3333
repeated bytes addrs = 2; // peer's known addresses
3434
}
3535

packages/protons/test/fixtures/circuit.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export namespace CircuitRelay {
9797

9898
if (opts.writeDefaults === true || (obj.id != null && obj.id.byteLength > 0)) {
9999
w.uint32(10)
100-
w.bytes(obj.id)
100+
w.bytes(obj.id ?? new Uint8Array(0))
101101
}
102102

103103
if (obj.addrs != null) {
@@ -141,7 +141,7 @@ export namespace CircuitRelay {
141141
return _codec
142142
}
143143

144-
export const encode = (obj: Peer): Uint8Array => {
144+
export const encode = (obj: Partial<Peer>): Uint8Array => {
145145
return encodeMessage(obj, Peer.codec())
146146
}
147147

@@ -220,7 +220,7 @@ export namespace CircuitRelay {
220220
return _codec
221221
}
222222

223-
export const encode = (obj: CircuitRelay): Uint8Array => {
223+
export const encode = (obj: Partial<CircuitRelay>): Uint8Array => {
224224
return encodeMessage(obj, CircuitRelay.codec())
225225
}
226226

0 commit comments

Comments
 (0)