From 8a0291096c6b9aab9050bf8b35958015d99e9470 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 17 Jan 2023 07:08:23 +0000 Subject: [PATCH] fix: support empty messages (#78) Empty messages should be supported --- packages/protons/src/index.ts | 204 +++++++++++---------- packages/protons/test/fixtures/basic.proto | 4 + packages/protons/test/fixtures/basic.ts | 47 +++++ packages/protons/test/fixtures/circuit.ts | 1 + packages/protons/test/fixtures/daemon.ts | 1 + packages/protons/test/fixtures/dht.ts | 1 + packages/protons/test/fixtures/maps.ts | 1 + packages/protons/test/fixtures/noise.ts | 45 +++++ packages/protons/test/fixtures/optional.ts | 1 + packages/protons/test/fixtures/peer.ts | 1 + packages/protons/test/fixtures/singular.ts | 1 + packages/protons/test/fixtures/test.ts | 1 + packages/protons/test/index.spec.ts | 8 +- 13 files changed, 216 insertions(+), 100 deletions(-) diff --git a/packages/protons/src/index.ts b/packages/protons/src/index.ts index 3ae6789..693ed4d 100644 --- a/packages/protons/src/index.ts +++ b/packages/protons/src/index.ts @@ -354,7 +354,10 @@ export namespace ${messageDef.name} { let interfaceDef = '' let interfaceCodecDef = '' - if (interfaceFields !== '') { + if (interfaceFields === '') { + interfaceDef = ` +export interface ${messageDef.name} {}` + } else { interfaceDef = ` export interface ${messageDef.name} { ${ @@ -363,72 +366,64 @@ export interface ${messageDef.name} { .trim() } }` + } - interfaceCodecDef = ` - let _codec: Codec<${messageDef.name}> - - export const codec = (): Codec<${messageDef.name}> => { - if (_codec == null) { - _codec = message<${messageDef.name}>((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() + const encodeFields = Object.entries(fields) + .map(([name, fieldDef]) => { + let codec: string = encoders[fieldDef.type] + let type: string = fieldDef.map ? 'message' : fieldDef.type + let typeName: string = '' + + if (codec == null) { + if (fieldDef.enum) { + moduleDef.imports.add('enumeration') + type = 'enum' + } else { + moduleDef.imports.add('message') + type = 'message' } -${Object.entries(fields) - .map(([name, fieldDef]) => { - let codec: string = encoders[fieldDef.type] - let type: string = fieldDef.map ? 'message' : fieldDef.type - let typeName: string = '' - if (codec == null) { - if (fieldDef.enum) { - moduleDef.imports.add('enumeration') - type = 'enum' - } else { - moduleDef.imports.add('message') - type = 'message' - } - - typeName = findTypeName(fieldDef.type, messageDef, moduleDef) - codec = `${typeName}.codec()` - } + typeName = findTypeName(fieldDef.type, messageDef, moduleDef) + codec = `${typeName}.codec()` + } - let valueTest = `obj.${name} != null` + let valueTest = `obj.${name} != null` - 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') { - // handle enums - valueTest = `opts.writeDefaults === true || (obj.${name} != null && __${fieldDef.type}Values[obj.${name}] !== 0)` - } + 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') { + // handle enums + valueTest = `opts.writeDefaults === true || (obj.${name} != null && __${fieldDef.type}Values[obj.${name}] !== 0)` } + } - function createWriteField (valueVar: string): string { - const id = (fieldDef.id << 3) | codecTypes[type] + function createWriteField (valueVar: string): string { + const id = (fieldDef.id << 3) | codecTypes[type] - let writeField = `w.uint32(${id}) + let writeField = `w.uint32(${id}) ${encoderGenerators[type] == null ? `${codec}.encode(${valueVar}, w)` : encoderGenerators[type](valueVar)}` - if (type === 'message') { - // message fields are only written if they have values - writeField = `w.uint32(${id}) + if (type === 'message') { + // message fields are only written if they have values + writeField = `w.uint32(${id}) ${typeName}.codec().encode(${valueVar}, w, { writeDefaults: ${Boolean(fieldDef.repeated).toString()} })` - } - - return writeField } - let writeField = createWriteField(`obj.${name}`) + return writeField + } - if (fieldDef.repeated) { - if (fieldDef.map) { - writeField = ` - for (const [key, value] of obj.${name}.entries()) { + let writeField = createWriteField(`obj.${name}`) + + if (fieldDef.repeated) { + if (fieldDef.map) { + writeField = ` + for (const [key, value] of obj.${name}.entries()) { ${ createWriteField('{ key, value }') .split('\n') @@ -440,9 +435,9 @@ ${Object.entries(fields) .join('\n') } } - `.trim() - } else { - writeField = ` + `.trim() + } else { + writeField = ` for (const value of obj.${name}) { ${ createWriteField('value') @@ -455,69 +450,80 @@ ${Object.entries(fields) .join('\n') } } - `.trim() - } + `.trim() } + } - return ` + return ` if (${valueTest}) { ${writeField} }` -}).join('\n')} + }).join('\n') - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {${createDefaultObject(fields, messageDef, moduleDef)}} + const decodeFields = Object.entries(fields) + .map(([fieldName, fieldDef]) => { + function createReadField (fieldName: string, fieldDef: FieldDef): string { + let codec: string = encoders[fieldDef.type] + let type: string = fieldDef.type - const end = length == null ? reader.len : reader.pos + length + if (codec == null) { + if (fieldDef.enum) { + moduleDef.imports.add('enumeration') + type = 'enum' + } else { + moduleDef.imports.add('message') + type = 'message' + } - while (reader.pos < end) { - const tag = reader.uint32() + const typeName = findTypeName(fieldDef.type, messageDef, moduleDef) + codec = `${typeName}.codec()` + } + + const parseValue = `${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()}` - switch (tag >>> 3) { - ${Object.entries(fields) - .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 parseValue = `${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type]()}` - - if (fieldDef.map) { - return `case ${fieldDef.id}: { + 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}: + } else if (fieldDef.repeated) { + return `case ${fieldDef.id}: obj.${fieldName}.push(${parseValue}) break` - } + } - return `case ${fieldDef.id}: + return `case ${fieldDef.id}: obj.${fieldName} = ${parseValue} break` - } + } - return createReadField(fieldName, fieldDef) - }) - .join('\n ')} + return createReadField(fieldName, fieldDef) + }) + .join('\n ') + + interfaceCodecDef = ` + let _codec: Codec<${messageDef.name}> + + export const codec = (): Codec<${messageDef.name}> => { + if (_codec == null) { + _codec = message<${messageDef.name}>((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } +${encodeFields === '' ? '' : `${encodeFields}\n`} + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {${createDefaultObject(fields, messageDef, moduleDef)}} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) {${decodeFields === '' ? '' : `\n ${decodeFields}`} default: reader.skipType(tag & 7) break @@ -538,7 +544,6 @@ ${Object.entries(fields) export const decode = (buf: Uint8Array | Uint8ArrayList): ${messageDef.name} => { return decodeMessage(buf, ${messageDef.name}.codec()) }` - } return ` ${interfaceDef} @@ -682,6 +687,7 @@ export async function generate (source: string, flags: Flags): Promise { '/* eslint-disable complexity */', '/* eslint-disable @typescript-eslint/no-namespace */', '/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */', + '/* eslint-disable @typescript-eslint/no-empty-interface */', '' ] diff --git a/packages/protons/test/fixtures/basic.proto b/packages/protons/test/fixtures/basic.proto index 83fbc41..fa24687 100644 --- a/packages/protons/test/fixtures/basic.proto +++ b/packages/protons/test/fixtures/basic.proto @@ -4,3 +4,7 @@ message Basic { optional string foo = 1; int32 num = 2; } + +message Empty { + +} diff --git a/packages/protons/test/fixtures/basic.ts b/packages/protons/test/fixtures/basic.ts index 49f18b2..e629eee 100644 --- a/packages/protons/test/fixtures/basic.ts +++ b/packages/protons/test/fixtures/basic.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' @@ -73,3 +74,49 @@ export namespace Basic { return decodeMessage(buf, Basic.codec()) } } + +export interface Empty {} + +export namespace Empty { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Empty): Uint8Array => { + return encodeMessage(obj, Empty.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Empty => { + return decodeMessage(buf, Empty.codec()) + } +} diff --git a/packages/protons/test/fixtures/circuit.ts b/packages/protons/test/fixtures/circuit.ts index e67461a..a0134c4 100644 --- a/packages/protons/test/fixtures/circuit.ts +++ b/packages/protons/test/fixtures/circuit.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/fixtures/daemon.ts b/packages/protons/test/fixtures/daemon.ts index 376f4f3..96b0eb2 100644 --- a/packages/protons/test/fixtures/daemon.ts +++ b/packages/protons/test/fixtures/daemon.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/fixtures/dht.ts b/packages/protons/test/fixtures/dht.ts index 4dd7698..f692c0c 100644 --- a/packages/protons/test/fixtures/dht.ts +++ b/packages/protons/test/fixtures/dht.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/fixtures/maps.ts b/packages/protons/test/fixtures/maps.ts index aab9f8b..8bd8a50 100644 --- a/packages/protons/test/fixtures/maps.ts +++ b/packages/protons/test/fixtures/maps.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/fixtures/noise.ts b/packages/protons/test/fixtures/noise.ts index fb0138c..86e0417 100644 --- a/packages/protons/test/fixtures/noise.ts +++ b/packages/protons/test/fixtures/noise.ts @@ -2,11 +2,14 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' +export interface pb {} + export namespace pb { export interface NoiseHandshakePayload { identityKey: Uint8Array @@ -85,4 +88,46 @@ export namespace pb { return decodeMessage(buf, NoiseHandshakePayload.codec()) } } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: pb): Uint8Array => { + return encodeMessage(obj, pb.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): pb => { + return decodeMessage(buf, pb.codec()) + } } diff --git a/packages/protons/test/fixtures/optional.ts b/packages/protons/test/fixtures/optional.ts index ec75092..91ff353 100644 --- a/packages/protons/test/fixtures/optional.ts +++ b/packages/protons/test/fixtures/optional.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/fixtures/peer.ts b/packages/protons/test/fixtures/peer.ts index a4e63b2..a55f9eb 100644 --- a/packages/protons/test/fixtures/peer.ts +++ b/packages/protons/test/fixtures/peer.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/fixtures/singular.ts b/packages/protons/test/fixtures/singular.ts index 7867ec7..ff3a65d 100644 --- a/packages/protons/test/fixtures/singular.ts +++ b/packages/protons/test/fixtures/singular.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/fixtures/test.ts b/packages/protons/test/fixtures/test.ts index 4598082..670d7c5 100644 --- a/packages/protons/test/fixtures/test.ts +++ b/packages/protons/test/fixtures/test.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/protons/test/index.spec.ts b/packages/protons/test/index.spec.ts index 08db22b..435814f 100644 --- a/packages/protons/test/index.spec.ts +++ b/packages/protons/test/index.spec.ts @@ -3,7 +3,7 @@ import { expect } from 'aegir/chai' import pbjs from 'pbjs' -import { Basic } from './fixtures/basic.js' +import { Basic, Empty } from './fixtures/basic.js' import { AllTheTypes, AnEnum } from './fixtures/test.js' import fs from 'fs' import protobufjs, { Type as PBType } from 'protobufjs' @@ -150,6 +150,12 @@ describe('encode', () => { testEncodings(obj, Basic, './test/fixtures/basic.proto', 'Basic') }) + it('should encode an empty message', () => { + const obj: Empty = {} + + testEncodings(obj, Empty, './test/fixtures/basic.proto', 'Empty') + }) + it('should encode all the types', () => { const obj: AllTheTypes = { field1: true,