Skip to content

Commit 6453809

Browse files
authored
feat: define default types during decode (#62)
Proto3 language guide [states](https://developers.google.com/protocol-buffers/docs/proto3#default): > When a message is parsed, if the encoded message does not contain a particular singular element, the corresponding field in the parsed object is set to the default value for that field. When decoding objects, create an object with all non-optional fields set to the default values, then overwrite them during decoding. Given that we create arrays for repeated fields there's no need to check for their existence before returning the deserialized object. The only weird part in all this is message fields. The spec says that any non-optional field is required, but it also says that the default value for a sub-message is to be unset and that the actual value there is language-dependent and links to some language guides which don't include JavaScript - I've used `undefined` here. The upshot of this is that you can have a non-optional field with a default value of `undefined` which essentially makes it optional. Nice. At the moment we throw if the sub-message is not present in the protobuf if the field is required though in a future change we may wish to ignore the spec and initialise the sub-message to an instance of it's type with default values set 🤷. Refs #43
1 parent a7d567d commit 6453809

File tree

12 files changed

+213
-176
lines changed

12 files changed

+213
-176
lines changed

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

+6-9
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ export namespace Yo {
159159
writer.ldelim()
160160
}
161161
}, (reader, length) => {
162-
const obj: any = {}
162+
const obj: any = {
163+
lol: []
164+
}
163165

164166
const end = length == null ? reader.len : reader.pos + length
165167

@@ -168,7 +170,6 @@ export namespace Yo {
168170

169171
switch (tag >>> 3) {
170172
case 1:
171-
obj.lol = obj.lol ?? []
172173
obj.lol.push(FOO.codec().decode(reader))
173174
break
174175
default:
@@ -177,12 +178,6 @@ export namespace Yo {
177178
}
178179
}
179180

180-
obj.lol = obj.lol ?? []
181-
182-
if (obj.lol == null) {
183-
throw new Error('Protocol error: value for required field "lol" was not found in protobuf')
184-
}
185-
186181
return obj
187182
})
188183
}
@@ -230,7 +225,9 @@ export namespace Lol {
230225
writer.ldelim()
231226
}
232227
}, (reader, length) => {
233-
const obj: any = {}
228+
const obj: any = {
229+
b: undefined
230+
}
234231

235232
const end = length == null ? reader.len : reader.pos + length
236233

packages/protons-runtime/src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,12 @@ export interface Reader {
180180
double: () => number
181181

182182
/**
183-
* Reads a sequence of bytes preceeded by its length as a varint
183+
* Reads a sequence of bytes preceded by its length as a varint
184184
*/
185-
bytes: () => number
185+
bytes: () => Uint8Array
186186

187187
/**
188-
* Reads a string preceeded by its byte length as a varint
188+
* Reads a string preceded by its byte length as a varint
189189
*/
190190
string: () => string
191191

packages/protons/src/index.ts

+83-6
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const encoderGenerators: Record<string, (val: string) => string> = {
3939
bool: (val) => `writer.bool(${val})`,
4040
bytes: (val) => `writer.bytes(${val})`,
4141
double: (val) => `writer.double(${val})`,
42-
// enumeration: (val) => `writer.double(${val})`,
4342
fixed32: (val) => `writer.fixed32(${val})`,
4443
fixed64: (val) => `writer.fixed64(${val})`,
4544
float: (val) => `writer.float(${val})`,
@@ -58,7 +57,6 @@ const decoderGenerators: Record<string, () => string> = {
5857
bool: () => 'reader.bool()',
5958
bytes: () => 'reader.bytes()',
6059
double: () => 'reader.double()',
61-
// enumeration: () => `writer.double(${val})`,
6260
fixed32: () => 'reader.fixed32()',
6361
fixed64: () => 'reader.fixed64()',
6462
float: () => 'reader.float()',
@@ -73,6 +71,24 @@ const decoderGenerators: Record<string, () => string> = {
7371
uint64: () => 'reader.uint64()'
7472
}
7573

74+
const defaultValueGenerators: Record<string, () => string> = {
75+
bool: () => 'false',
76+
bytes: () => 'new Uint8Array(0)',
77+
double: () => '0',
78+
fixed32: () => '0',
79+
fixed64: () => '0n',
80+
float: () => '0',
81+
int32: () => '0',
82+
int64: () => '0n',
83+
sfixed32: () => '0',
84+
sfixed64: () => '0n',
85+
sint32: () => '0',
86+
sint64: () => '0n',
87+
string: () => "''",
88+
uint32: () => '0',
89+
uint64: () => '0n'
90+
}
91+
7692
function findTypeName (typeName: string, classDef: MessageDef, moduleDef: ModuleDef): string {
7793
if (types[typeName] != null) {
7894
return types[typeName]
@@ -117,6 +133,65 @@ function findDef (typeName: string, classDef: MessageDef, moduleDef: ModuleDef):
117133
throw new Error(`Could not resolve type name "${typeName}"`)
118134
}
119135

136+
function createDefaultObject (fields: Record<string, FieldDef>, messageDef: MessageDef, moduleDef: ModuleDef): string {
137+
const output = Object.entries(fields)
138+
.map(([name, fieldDef]) => {
139+
if (fieldDef.repeated) {
140+
return `${name}: []`
141+
}
142+
143+
if (fieldDef.optional) {
144+
return ''
145+
}
146+
147+
const type: string = fieldDef.type
148+
let defaultValue
149+
150+
if (defaultValueGenerators[type] != null) {
151+
defaultValue = defaultValueGenerators[type]()
152+
} else {
153+
const def = findDef(fieldDef.type, messageDef, moduleDef)
154+
155+
if (isEnumDef(def)) {
156+
// select lowest-value enum - should be 0 but it's not guaranteed
157+
const val = Object.entries(def.values)
158+
.sort((a, b) => {
159+
if (a[1] < b[1]) {
160+
return 1
161+
}
162+
163+
if (a[1] > b[1]) {
164+
return -1
165+
}
166+
167+
return 0
168+
})
169+
.pop()
170+
171+
if (val == null) {
172+
throw new Error(`Could not find default enum value for ${def.fullName}`)
173+
}
174+
175+
defaultValue = `${def.name}.${val[0]}`
176+
} else {
177+
defaultValue = 'undefined'
178+
}
179+
}
180+
181+
return `${name}: ${defaultValue}`
182+
})
183+
.filter(Boolean)
184+
.join(',\n ')
185+
186+
if (output !== '') {
187+
return `
188+
${output}
189+
`
190+
}
191+
192+
return ''
193+
}
194+
120195
const encoders: Record<string, string> = {
121196
bool: 'bool',
122197
bytes: 'bytes',
@@ -259,7 +334,7 @@ export interface ${messageDef.name} {
259334
const ensureArrayProps = Object.entries(fields)
260335
.map(([name, fieldDef]) => {
261336
// make sure repeated fields have an array if not set
262-
if (fieldDef.rule === 'repeated') {
337+
if (fieldDef.optional && fieldDef.rule === 'repeated') {
263338
return ` obj.${name} = obj.${name} ?? []`
264339
}
265340

@@ -269,7 +344,7 @@ export interface ${messageDef.name} {
269344
const ensureRequiredFields = Object.entries(fields)
270345
.map(([name, fieldDef]) => {
271346
// make sure required fields are set
272-
if (!fieldDef.optional) {
347+
if (!fieldDef.optional && !fieldDef.repeated) {
273348
return `
274349
if (obj.${name} == null) {
275350
throw new Error('Protocol error: value for required field "${name}" was not found in protobuf')
@@ -331,7 +406,7 @@ ${Object.entries(fields)
331406
writer.ldelim()
332407
}
333408
}, (reader, length) => {
334-
const obj: any = {}
409+
const obj: any = {${createDefaultObject(fields, messageDef, moduleDef)}}
335410
336411
const end = length == null ? reader.len : reader.pos + length
337412
@@ -360,8 +435,10 @@ ${Object.entries(fields)
360435
}
361436
362437
return `case ${fieldDef.id}:${fieldDef.rule === 'repeated'
438+
? `${fieldDef.optional
363439
? `
364-
obj.${name} = obj.${name} ?? []
440+
obj.${name} = obj.${name} ?? []`
441+
: ''}
365442
obj.${name}.push(${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()})`
366443
: `
367444
obj.${name} = ${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()}`}

packages/protons/test/fixtures/basic.proto

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ syntax = "proto3";
22

33
message Basic {
44
optional string foo = 1;
5-
required int32 num = 2;
5+
int32 num = 2;
66
}

packages/protons/test/fixtures/basic.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export namespace Basic {
3636
writer.ldelim()
3737
}
3838
}, (reader, length) => {
39-
const obj: any = {}
39+
const obj: any = {
40+
num: 0
41+
}
4042

4143
const end = length == null ? reader.len : reader.pos + length
4244

packages/protons/test/fixtures/circuit.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ export namespace CircuitRelay {
112112
writer.ldelim()
113113
}
114114
}, (reader, length) => {
115-
const obj: any = {}
115+
const obj: any = {
116+
id: new Uint8Array(0),
117+
addrs: []
118+
}
116119

117120
const end = length == null ? reader.len : reader.pos + length
118121

@@ -124,7 +127,6 @@ export namespace CircuitRelay {
124127
obj.id = reader.bytes()
125128
break
126129
case 2:
127-
obj.addrs = obj.addrs ?? []
128130
obj.addrs.push(reader.bytes())
129131
break
130132
default:
@@ -133,16 +135,10 @@ export namespace CircuitRelay {
133135
}
134136
}
135137

136-
obj.addrs = obj.addrs ?? []
137-
138138
if (obj.id == null) {
139139
throw new Error('Protocol error: value for required field "id" was not found in protobuf')
140140
}
141141

142-
if (obj.addrs == null) {
143-
throw new Error('Protocol error: value for required field "addrs" was not found in protobuf')
144-
}
145-
146142
return obj
147143
})
148144
}

0 commit comments

Comments
 (0)