diff --git a/bin/pbf b/bin/pbf index 7981f06..aabd240 100755 --- a/bin/pbf +++ b/bin/pbf @@ -4,13 +4,14 @@ import resolve from 'resolve-protobuf-schema'; import {compileRaw} from '../compile.js'; if (process.argv.length < 3) { - console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--legacy]'); + console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--jsdoc] [--legacy]'); process.exit(0); } const code = compileRaw(resolve.sync(process.argv[2]), { noRead: process.argv.indexOf('--no-read') >= 0, noWrite: process.argv.indexOf('--no-write') >= 0, + jsDoc: process.argv.indexOf('--jsdoc') >= 0, legacy: process.argv.indexOf('--legacy') >= 0 }); diff --git a/compile.js b/compile.js index 220de6d..7a7be29 100644 --- a/compile.js +++ b/compile.js @@ -9,7 +9,13 @@ export function compile(proto) { export function compileRaw(proto, options = {}) { const context = buildDefaults(buildContext(proto, null), proto.syntax); - return `${options.dev ? '' : `// code generated by pbf v${version}\n`}${writeContext(context, options)}`; + + let output = options.dev ? '' : `// code generated by pbf v${version}\n`; + if (options.jsDoc) { + output += typeDef(`import("${options.dev ? '../../index.js' : 'pbf'}").default`, 'Pbf'); + } + output += writeContext(context, options); + return output; } function writeContext(ctx, options) { @@ -24,17 +30,30 @@ function writeContext(ctx, options) { } function writeMessage(ctx, options) { + const name = ctx._name; const fields = ctx._proto.fields; let code = '\n'; + if (options.jsDoc) { + code += compileObjectType(ctx, name, fields); + } + if (!options.noRead) { - const readName = `read${ctx._name}`; + const readName = `read${name}`; + if (options.jsDoc) { + code += ['\n/**', ' * @param {Pbf} pbf', ' * @param {number} [end]', ` * @returns {${name}}`, ' */'].join('\n').concat('\n'); + } code += `${writeFunctionExport(options, readName)}function ${readName}(pbf, end) { return pbf.readFields(${readName}Field, ${compileDest(ctx)}, end); -} -function ${readName}Field(tag, obj, pbf) { +}\n`; + + if (options.jsDoc) { + code += ['\n/**', ' * @param {number} tag', ` * @param {${name}} obj`, ' * @param {Pbf} pbf', ' */'].join('\n').concat('\n'); + } + code += +`function ${readName}Field(tag, obj, pbf) { `; for (let i = 0; i < fields.length; i++) { const field = fields[i]; @@ -60,7 +79,12 @@ function ${readName}Field(tag, obj, pbf) { } if (!options.noWrite) { - const writeName = `write${ctx._name}`; + const writeName = `write${name}`; + + if (options.jsDoc) { + code += ['\n/**', ` * @param {${name}} obj`, ' * @param {Pbf} pbf', ' */'].join('\n').concat('\n'); + } + code += `${writeFunctionExport(options, writeName)}function ${writeName}(obj, pbf) {\n`; for (const field of fields) { const writeCode = @@ -90,7 +114,10 @@ function getEnumValues(ctx) { function writeEnum(ctx, {legacy}) { const enums = JSON.stringify(getEnumValues(ctx), null, 4); const name = ctx._name; - return `\n${legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`; + + let output = '\n/** @enum {number} */'; + output += `\n${legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`; + return output; } function compileDest(ctx) { @@ -114,23 +141,27 @@ function getType(ctx, field) { return path.reduce((ctx, name) => ctx && ctx[name], ctx); } +function fieldTypeIsNumber(field) { + switch (field.type) { + case 'float': + case 'double': + case 'uint32': + case 'uint64': + case 'int32': + case 'int64': + case 'sint32': + case 'sint64': + case 'fixed32': + case 'fixed64': + case 'sfixed32': + case 'sfixed64': return true; + default: return false; + } +} + function fieldShouldUseStringAsNumber(field) { if (field.options.jstype === 'JS_STRING') { - switch (field.type) { - case 'float': - case 'double': - case 'uint32': - case 'uint64': - case 'int32': - case 'int64': - case 'sint32': - case 'sint64': - case 'fixed32': - case 'fixed64': - case 'sfixed32': - case 'sfixed64': return true; - default: return false; - } + return fieldTypeIsNumber(field); } return false; } @@ -415,3 +446,63 @@ function getDefaultWriteTest(ctx, field) { function isPacked(field) { return field.options.packed === 'true'; } + +/** + * @param {{type: string}} field + * @returns {string} + */ +function getTypeScriptType(field) { + let type = field.type; + + if (fieldShouldUseStringAsNumber(field)) type = 'string'; + else if (fieldTypeIsNumber(field)) type = 'number'; + else if (field.type === 'bytes') type = 'Uint8Array'; + else if (field.type === 'bool') type = 'boolean'; + + return field.repeated ? `Array<${type}>` : type; +} + +/** + * @param {string} type + * @param {string} name + * @param {{name: string; type: string; required: boolean}} [fields] + * @returns {string} + */ +function typeDef(type, name, fields = []) { + const oneofs = {}; + + const properties = fields.map((field) => { + if (field.oneof) { + oneofs[field.oneof] = oneofs[field.oneof] || []; + oneofs[field.oneof].push(field.name); + } + + const type = getTypeScriptType(field); + const name = (field.required || field.repeated) ? field.name : `[${field.name}]`; + return ` * @property {${type}} ${name}`; + }); + + for (const oneof in oneofs) { + const subtypes = oneofs[oneof].map(s => `${name}["${s}"]`).join(' | '); + properties.push(` * @property {${subtypes}} ${oneof}`); + } + + return ['/**', ` * @typedef {${type}} ${name}`, ...properties, ' */'] + .join('\n') + .concat('\n'); +} + +/** + * @param {object} ctx + * @param {string} name + * @param {{name: string; type: string; required: boolean}} [fields] + * @returns {string} + */ +function compileObjectType(ctx, name, fields = []) { + const typedFields = fields.map((field) => { + const type = getType(ctx, field); + return {...field, type: type ? type._name : field.type}; + }); + + return typeDef('object', name, typedFields); +} diff --git a/package-lock.json b/package-lock.json index 5f57878..441b90e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "pbf", "version": "4.0.1", "license": "BSD-3-Clause", + "workspaces": [ + "test/typescript/" + ], "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, @@ -1760,6 +1763,14 @@ "node": ">=8" } }, + "node_modules/pbf": { + "resolved": "", + "link": true + }, + "node_modules/pbf-ts": { + "resolved": "test/typescript", + "link": true + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -2441,6 +2452,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "test/typescript": { + "dependencies": { + "pbf": "file:../../" + }, + "devDependencies": { + "typescript": "^5.5.3" + } } } } diff --git a/package.json b/package.json index 499b0f6..8e4e76a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "bench": "node bench/bench.js", "pretest": "eslint *.js compile.js test/*.js test/fixtures/*.js bin/pbf", - "test": "tsc && node --test", + "test": "tsc && node --test && tsc --allowJs --checkJs --noEmit --isolatedModules test/fixtures/*.js", "cov": "node --test --experimental-test-covetage", "build": "rollup -c", "prepublishOnly": "npm run test && npm run build" @@ -60,5 +60,8 @@ "rollup": "^4.18.0", "tile-stats-runner": "^1.0.0", "typescript": "^5.5.3" - } + }, + "workspaces": [ + "test/typescript/" + ] } diff --git a/test/compile.test.js b/test/compile.test.js index d2e5157..75dbfab 100644 --- a/test/compile.test.js +++ b/test/compile.test.js @@ -10,9 +10,9 @@ test('compiles all proto files to proper js', () => { const files = fs.readdirSync(new URL('fixtures', import.meta.url)); for (const path of files) { - if (!path.endsWith('.proto')) continue; + if (!path.endsWith('oneof.proto')) continue; const proto = resolve(new URL(`fixtures/${path}`, import.meta.url)); - const js = compileRaw(proto, {dev: true}); + const js = compileRaw(proto, {dev: true, jsDoc: true}); // uncomment to update the fixtures // fs.writeFileSync(new URL(`fixtures/${path}`.replace('.proto', '.js'), import.meta.url), js); diff --git a/test/fixtures/defaults.js b/test/fixtures/defaults.js index bf0ef32..af9079c 100644 --- a/test/fixtures/defaults.js +++ b/test/fixtures/defaults.js @@ -1,12 +1,36 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** @enum {number} */ export const MessageType = { "UNKNOWN": 0, "GREETING": 1 }; +/** + * @typedef {object} Envelope + * @property {MessageType} [type] + * @property {string} [name] + * @property {boolean} [flag] + * @property {number} [weight] + * @property {number} [id] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope} + */ export function readEnvelope(pbf, end) { return pbf.readFields(readEnvelopeField, {type: 1, name: "test", flag: true, weight: 1.5, id: 1}, end); } + +/** + * @param {number} tag + * @param {Envelope} obj + * @param {Pbf} pbf + */ function readEnvelopeField(tag, obj, pbf) { if (tag === 1) obj.type = pbf.readVarint(); else if (tag === 2) obj.name = pbf.readString(); @@ -14,6 +38,11 @@ function readEnvelopeField(tag, obj, pbf) { else if (tag === 4) obj.weight = pbf.readFloat(); else if (tag === 5) obj.id = pbf.readVarint(true); } + +/** + * @param {Envelope} obj + * @param {Pbf} pbf + */ export function writeEnvelope(obj, pbf) { if (obj.type != null && obj.type !== 1) pbf.writeVarintField(1, obj.type); if (obj.name != null && obj.name !== "test") pbf.writeStringField(2, obj.name); diff --git a/test/fixtures/defaults_implicit.js b/test/fixtures/defaults_implicit.js index 5603350..54d0573 100644 --- a/test/fixtures/defaults_implicit.js +++ b/test/fixtures/defaults_implicit.js @@ -1,20 +1,69 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** @enum {number} */ export const MessageType = { "UNKNOWN": 0, "GREETING": 1 }; +/** + * @typedef {object} CustomType + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {CustomType} + */ export function readCustomType(pbf, end) { return pbf.readFields(readCustomTypeField, {}, end); } + +/** + * @param {number} tag + * @param {CustomType} obj + * @param {Pbf} pbf + */ function readCustomTypeField(tag, obj, pbf) { } + +/** + * @param {CustomType} obj + * @param {Pbf} pbf + */ export function writeCustomType(obj, pbf) { } +/** + * @typedef {object} Envelope + * @property {MessageType} [type] + * @property {string} [name] + * @property {boolean} [flag] + * @property {number} [weight] + * @property {number} [id] + * @property {Array} tags + * @property {Array} numbers + * @property {Uint8Array} [bytes] + * @property {CustomType} [custom] + * @property {Array} types + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope} + */ export function readEnvelope(pbf, end) { return pbf.readFields(readEnvelopeField, {type: 0, name: "", flag: false, weight: 0, id: 0, tags: [], numbers: [], bytes: undefined, custom: undefined, types: []}, end); } + +/** + * @param {number} tag + * @param {Envelope} obj + * @param {Pbf} pbf + */ function readEnvelopeField(tag, obj, pbf) { if (tag === 1) obj.type = pbf.readVarint(); else if (tag === 2) obj.name = pbf.readString(); @@ -27,6 +76,11 @@ function readEnvelopeField(tag, obj, pbf) { else if (tag === 9) obj.custom = readCustomType(pbf, pbf.readVarint() + pbf.pos); else if (tag === 10) pbf.readPackedVarint(obj.types); } + +/** + * @param {Envelope} obj + * @param {Pbf} pbf + */ export function writeEnvelope(obj, pbf) { if (obj.type) pbf.writeVarintField(1, obj.type); if (obj.name) pbf.writeStringField(2, obj.name); diff --git a/test/fixtures/defaults_proto3.js b/test/fixtures/defaults_proto3.js index 57748fb..289b16d 100644 --- a/test/fixtures/defaults_proto3.js +++ b/test/fixtures/defaults_proto3.js @@ -1,12 +1,36 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** @enum {number} */ export const MessageType = { "UNKNOWN": 0, "GREETING": 1 }; +/** + * @typedef {object} Envelope + * @property {MessageType} [type] + * @property {string} [name] + * @property {boolean} [flag] + * @property {number} [weight] + * @property {number} [id] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope} + */ export function readEnvelope(pbf, end) { return pbf.readFields(readEnvelopeField, {type: 0, name: "", flag: false, weight: 0, id: 0}, end); } + +/** + * @param {number} tag + * @param {Envelope} obj + * @param {Pbf} pbf + */ function readEnvelopeField(tag, obj, pbf) { if (tag === 1) obj.type = pbf.readVarint(); else if (tag === 2) obj.name = pbf.readString(); @@ -14,6 +38,11 @@ function readEnvelopeField(tag, obj, pbf) { else if (tag === 4) obj.weight = pbf.readFloat(); else if (tag === 5) obj.id = pbf.readVarint(true); } + +/** + * @param {Envelope} obj + * @param {Pbf} pbf + */ export function writeEnvelope(obj, pbf) { if (obj.type) pbf.writeVarintField(1, obj.type); if (obj.name) pbf.writeStringField(2, obj.name); diff --git a/test/fixtures/embedded_type.js b/test/fixtures/embedded_type.js index 4c41ddf..e355c21 100644 --- a/test/fixtures/embedded_type.js +++ b/test/fixtures/embedded_type.js @@ -1,34 +1,102 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** + * @typedef {object} EmbeddedType + * @property {string} [value] + * @property {EmbeddedTypeContainer} [sub_field] + * @property {EmbeddedTypeContainerInner} [sub_sub_field] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {EmbeddedType} + */ export function readEmbeddedType(pbf, end) { return pbf.readFields(readEmbeddedTypeField, {value: "test", sub_field: undefined, sub_sub_field: undefined}, end); } + +/** + * @param {number} tag + * @param {EmbeddedType} obj + * @param {Pbf} pbf + */ function readEmbeddedTypeField(tag, obj, pbf) { if (tag === 1) obj.value = pbf.readString(); else if (tag === 4) obj.sub_field = readEmbeddedTypeContainer(pbf, pbf.readVarint() + pbf.pos); else if (tag === 5) obj.sub_sub_field = readEmbeddedTypeContainerInner(pbf, pbf.readVarint() + pbf.pos); } + +/** + * @param {EmbeddedType} obj + * @param {Pbf} pbf + */ export function writeEmbeddedType(obj, pbf) { if (obj.value != null && obj.value !== "test") pbf.writeStringField(1, obj.value); if (obj.sub_field) pbf.writeMessage(4, writeEmbeddedTypeContainer, obj.sub_field); if (obj.sub_sub_field) pbf.writeMessage(5, writeEmbeddedTypeContainerInner, obj.sub_sub_field); } +/** + * @typedef {object} EmbeddedTypeContainer + * @property {Array} values + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {EmbeddedTypeContainer} + */ export function readEmbeddedTypeContainer(pbf, end) { return pbf.readFields(readEmbeddedTypeContainerField, {values: []}, end); } + +/** + * @param {number} tag + * @param {EmbeddedTypeContainer} obj + * @param {Pbf} pbf + */ function readEmbeddedTypeContainerField(tag, obj, pbf) { if (tag === 1) obj.values.push(readEmbeddedTypeContainerInner(pbf, pbf.readVarint() + pbf.pos)); } + +/** + * @param {EmbeddedTypeContainer} obj + * @param {Pbf} pbf + */ export function writeEmbeddedTypeContainer(obj, pbf) { if (obj.values) for (const item of obj.values) pbf.writeMessage(1, writeEmbeddedTypeContainerInner, item); } +/** + * @typedef {object} EmbeddedTypeContainerInner + * @property {string} [value] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {EmbeddedTypeContainerInner} + */ export function readEmbeddedTypeContainerInner(pbf, end) { return pbf.readFields(readEmbeddedTypeContainerInnerField, {value: ""}, end); } + +/** + * @param {number} tag + * @param {EmbeddedTypeContainerInner} obj + * @param {Pbf} pbf + */ function readEmbeddedTypeContainerInnerField(tag, obj, pbf) { if (tag === 1) obj.value = pbf.readString(); } + +/** + * @param {EmbeddedTypeContainerInner} obj + * @param {Pbf} pbf + */ export function writeEmbeddedTypeContainerInner(obj, pbf) { if (obj.value) pbf.writeStringField(1, obj.value); } diff --git a/test/fixtures/map.js b/test/fixtures/map.js index 793d26c..ff799f0 100644 --- a/test/fixtures/map.js +++ b/test/fixtures/map.js @@ -1,35 +1,104 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** + * @typedef {object} Envelope + * @property {Envelope_FieldEntry1} [kv] + * @property {Envelope_FieldEntry2} [kn] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope} + */ export function readEnvelope(pbf, end) { return pbf.readFields(readEnvelopeField, {kv: {}, kn: {}}, end); } + +/** + * @param {number} tag + * @param {Envelope} obj + * @param {Pbf} pbf + */ function readEnvelopeField(tag, obj, pbf) { if (tag === 1) { const {key, value} = readEnvelope_FieldEntry1(pbf, pbf.readVarint() + pbf.pos); obj.kv[key] = value; } else if (tag === 2) { const {key, value} = readEnvelope_FieldEntry2(pbf, pbf.readVarint() + pbf.pos); obj.kn[key] = value; } } + +/** + * @param {Envelope} obj + * @param {Pbf} pbf + */ export function writeEnvelope(obj, pbf) { if (obj.kv) for (const key of Object.keys(obj.kv)) pbf.writeMessage(1, writeEnvelope_FieldEntry1, {key, value: obj.kv[key]}); if (obj.kn) for (const key of Object.keys(obj.kn)) pbf.writeMessage(2, writeEnvelope_FieldEntry2, {key, value: obj.kn[key]}); } +/** + * @typedef {object} Envelope_FieldEntry1 + * @property {string} [key] + * @property {string} [value] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope_FieldEntry1} + */ export function readEnvelope_FieldEntry1(pbf, end) { return pbf.readFields(readEnvelope_FieldEntry1Field, {key: "", value: ""}, end); } + +/** + * @param {number} tag + * @param {Envelope_FieldEntry1} obj + * @param {Pbf} pbf + */ function readEnvelope_FieldEntry1Field(tag, obj, pbf) { if (tag === 1) obj.key = pbf.readString(); else if (tag === 2) obj.value = pbf.readString(); } + +/** + * @param {Envelope_FieldEntry1} obj + * @param {Pbf} pbf + */ export function writeEnvelope_FieldEntry1(obj, pbf) { if (obj.key) pbf.writeStringField(1, obj.key); if (obj.value) pbf.writeStringField(2, obj.value); } +/** + * @typedef {object} Envelope_FieldEntry2 + * @property {string} [key] + * @property {number} [value] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope_FieldEntry2} + */ export function readEnvelope_FieldEntry2(pbf, end) { return pbf.readFields(readEnvelope_FieldEntry2Field, {key: "", value: 0}, end); } + +/** + * @param {number} tag + * @param {Envelope_FieldEntry2} obj + * @param {Pbf} pbf + */ function readEnvelope_FieldEntry2Field(tag, obj, pbf) { if (tag === 1) obj.key = pbf.readString(); else if (tag === 2) obj.value = pbf.readVarint(true); } + +/** + * @param {Envelope_FieldEntry2} obj + * @param {Pbf} pbf + */ export function writeEnvelope_FieldEntry2(obj, pbf) { if (obj.key) pbf.writeStringField(1, obj.key); if (obj.value) pbf.writeVarintField(2, obj.value); diff --git a/test/fixtures/oneof.js b/test/fixtures/oneof.js index 5055689..ca02c3e 100644 --- a/test/fixtures/oneof.js +++ b/test/fixtures/oneof.js @@ -1,13 +1,41 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** + * @typedef {object} Envelope + * @property {number} [id] + * @property {number} [int] + * @property {number} [float] + * @property {string} [string] + * @property {Envelope["int"] | Envelope["float"] | Envelope["string"]} value + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope} + */ export function readEnvelope(pbf, end) { return pbf.readFields(readEnvelopeField, {id: 0, int: 0, value: undefined, float: 0, string: ""}, end); } + +/** + * @param {number} tag + * @param {Envelope} obj + * @param {Pbf} pbf + */ function readEnvelopeField(tag, obj, pbf) { if (tag === 1) obj.id = pbf.readVarint(true); else if (tag === 2) { obj.int = pbf.readVarint(true); obj.value = "int"; } else if (tag === 3) { obj.float = pbf.readFloat(); obj.value = "float"; } else if (tag === 4) { obj.string = pbf.readString(); obj.value = "string"; } } + +/** + * @param {Envelope} obj + * @param {Pbf} pbf + */ export function writeEnvelope(obj, pbf) { if (obj.id) pbf.writeVarintField(1, obj.id); if (obj.int != null) pbf.writeVarintField(2, obj.int); diff --git a/test/fixtures/packed.js b/test/fixtures/packed.js index 6780737..9340091 100644 --- a/test/fixtures/packed.js +++ b/test/fixtures/packed.js @@ -1,35 +1,104 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** + * @typedef {object} NotPacked + * @property {Array} value + * @property {Array} types + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {NotPacked} + */ export function readNotPacked(pbf, end) { return pbf.readFields(readNotPackedField, {value: [], types: []}, end); } + +/** + * @param {number} tag + * @param {NotPacked} obj + * @param {Pbf} pbf + */ function readNotPackedField(tag, obj, pbf) { if (tag === 1) pbf.readPackedVarint(obj.value, true); else if (tag === 2) pbf.readPackedVarint(obj.types, true); } + +/** + * @param {NotPacked} obj + * @param {Pbf} pbf + */ export function writeNotPacked(obj, pbf) { if (obj.value) for (const item of obj.value) pbf.writeVarintField(1, item); if (obj.types) for (const item of obj.types) pbf.writeVarintField(2, item); } +/** + * @typedef {object} FalsePacked + * @property {Array} value + * @property {Array} types + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {FalsePacked} + */ export function readFalsePacked(pbf, end) { return pbf.readFields(readFalsePackedField, {value: [], types: []}, end); } + +/** + * @param {number} tag + * @param {FalsePacked} obj + * @param {Pbf} pbf + */ function readFalsePackedField(tag, obj, pbf) { if (tag === 1) pbf.readPackedVarint(obj.value, true); else if (tag === 2) pbf.readPackedVarint(obj.types, true); } + +/** + * @param {FalsePacked} obj + * @param {Pbf} pbf + */ export function writeFalsePacked(obj, pbf) { if (obj.value) for (const item of obj.value) pbf.writeVarintField(1, item); if (obj.types) for (const item of obj.types) pbf.writeVarintField(2, item); } +/** + * @typedef {object} Packed + * @property {Array} value + * @property {Array} types + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Packed} + */ export function readPacked(pbf, end) { return pbf.readFields(readPackedField, {value: [], types: []}, end); } + +/** + * @param {number} tag + * @param {Packed} obj + * @param {Pbf} pbf + */ function readPackedField(tag, obj, pbf) { if (tag === 1) pbf.readPackedVarint(obj.value, true); else if (tag === 2) pbf.readPackedVarint(obj.types, true); } + +/** + * @param {Packed} obj + * @param {Pbf} pbf + */ export function writePacked(obj, pbf) { if (obj.value) pbf.writePackedVarint(1, obj.value); if (obj.types) pbf.writePackedVarint(2, obj.types); diff --git a/test/fixtures/packed_proto3.js b/test/fixtures/packed_proto3.js index 3584f4d..d0daee2 100644 --- a/test/fixtures/packed_proto3.js +++ b/test/fixtures/packed_proto3.js @@ -1,39 +1,108 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** @enum {number} */ export const MessageType = { "UNKNOWN": 0, "GREETING": 1 }; +/** + * @typedef {object} NotPacked + * @property {Array} value + * @property {Array} types + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {NotPacked} + */ export function readNotPacked(pbf, end) { return pbf.readFields(readNotPackedField, {value: [], types: []}, end); } + +/** + * @param {number} tag + * @param {NotPacked} obj + * @param {Pbf} pbf + */ function readNotPackedField(tag, obj, pbf) { if (tag === 1) pbf.readPackedVarint(obj.value, true); else if (tag === 2) pbf.readPackedVarint(obj.types); } + +/** + * @param {NotPacked} obj + * @param {Pbf} pbf + */ export function writeNotPacked(obj, pbf) { if (obj.value) pbf.writePackedVarint(1, obj.value); if (obj.types) pbf.writePackedVarint(2, obj.types); } +/** + * @typedef {object} FalsePacked + * @property {Array} value + * @property {Array} types + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {FalsePacked} + */ export function readFalsePacked(pbf, end) { return pbf.readFields(readFalsePackedField, {value: [], types: []}, end); } + +/** + * @param {number} tag + * @param {FalsePacked} obj + * @param {Pbf} pbf + */ function readFalsePackedField(tag, obj, pbf) { if (tag === 1) pbf.readPackedVarint(obj.value, true); else if (tag === 2) pbf.readPackedVarint(obj.types); } + +/** + * @param {FalsePacked} obj + * @param {Pbf} pbf + */ export function writeFalsePacked(obj, pbf) { if (obj.value) for (const item of obj.value) pbf.writeVarintField(1, item); if (obj.types) for (const item of obj.types) pbf.writeVarintField(2, item); } +/** + * @typedef {object} Packed + * @property {Array} value + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Packed} + */ export function readPacked(pbf, end) { return pbf.readFields(readPackedField, {value: []}, end); } + +/** + * @param {number} tag + * @param {Packed} obj + * @param {Pbf} pbf + */ function readPackedField(tag, obj, pbf) { if (tag === 16) pbf.readPackedVarint(obj.value, true); } + +/** + * @param {Packed} obj + * @param {Pbf} pbf + */ export function writePacked(obj, pbf) { if (obj.value) pbf.writePackedVarint(16, obj.value); } diff --git a/test/fixtures/type_string.js b/test/fixtures/type_string.js index 9c16b01..a67abbf 100644 --- a/test/fixtures/type_string.js +++ b/test/fixtures/type_string.js @@ -1,7 +1,31 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** + * @typedef {object} TypeString + * @property {string} [int] + * @property {string} [long] + * @property {boolean} [boolVal] + * @property {string} [float] + * @property {string} [default_implicit] + * @property {string} [default_explicit] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {TypeString} + */ export function readTypeString(pbf, end) { return pbf.readFields(readTypeStringField, {int: "0", long: "0", boolVal: false, float: "0", default_implicit: "0", default_explicit: "42"}, end); } + +/** + * @param {number} tag + * @param {TypeString} obj + * @param {Pbf} pbf + */ function readTypeStringField(tag, obj, pbf) { if (tag === 1) obj.int = pbf.readVarint(true).toString(); else if (tag === 2) obj.long = pbf.readVarint(true).toString(); @@ -10,6 +34,11 @@ function readTypeStringField(tag, obj, pbf) { else if (tag === 5) obj.default_implicit = pbf.readVarint(true).toString(); else if (tag === 6) obj.default_explicit = pbf.readVarint(true).toString(); } + +/** + * @param {TypeString} obj + * @param {Pbf} pbf + */ export function writeTypeString(obj, pbf) { if (obj.int != null && obj.int !== "0") pbf.writeVarintField(1, parseInt(obj.int, 10)); if (obj.long != null && obj.long !== "0") pbf.writeVarintField(2, parseInt(obj.long, 10)); @@ -19,15 +48,39 @@ export function writeTypeString(obj, pbf) { if (obj.default_explicit != null && obj.default_explicit !== "42") pbf.writeVarintField(6, parseInt(obj.default_explicit, 10)); } +/** + * @typedef {object} TypeNotString + * @property {number} [int] + * @property {number} [long] + * @property {boolean} [boolVal] + * @property {number} [float] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {TypeNotString} + */ export function readTypeNotString(pbf, end) { return pbf.readFields(readTypeNotStringField, {int: 0, long: 0, boolVal: false, float: 0}, end); } + +/** + * @param {number} tag + * @param {TypeNotString} obj + * @param {Pbf} pbf + */ function readTypeNotStringField(tag, obj, pbf) { if (tag === 1) obj.int = pbf.readVarint(true); else if (tag === 2) obj.long = pbf.readVarint(true); else if (tag === 3) obj.boolVal = pbf.readBoolean(); else if (tag === 4) obj.float = pbf.readFloat(); } + +/** + * @param {TypeNotString} obj + * @param {Pbf} pbf + */ export function writeTypeNotString(obj, pbf) { if (obj.int) pbf.writeVarintField(1, obj.int); if (obj.long) pbf.writeVarintField(2, obj.long); diff --git a/test/fixtures/varint.js b/test/fixtures/varint.js index 7cebe50..f80e711 100644 --- a/test/fixtures/varint.js +++ b/test/fixtures/varint.js @@ -1,13 +1,40 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** + * @typedef {object} Envelope + * @property {number} [int] + * @property {number} [uint] + * @property {number} [long] + * @property {number} [ulong] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Envelope} + */ export function readEnvelope(pbf, end) { return pbf.readFields(readEnvelopeField, {int: 0, uint: 0, long: 0, ulong: 0}, end); } + +/** + * @param {number} tag + * @param {Envelope} obj + * @param {Pbf} pbf + */ function readEnvelopeField(tag, obj, pbf) { if (tag === 1) obj.int = pbf.readVarint(true); else if (tag === 2) obj.uint = pbf.readVarint(); else if (tag === 3) obj.long = pbf.readVarint(true); else if (tag === 4) obj.ulong = pbf.readVarint(); } + +/** + * @param {Envelope} obj + * @param {Pbf} pbf + */ export function writeEnvelope(obj, pbf) { if (obj.int) pbf.writeVarintField(1, obj.int); if (obj.uint) pbf.writeVarintField(2, obj.uint); diff --git a/test/fixtures/vector_tile.js b/test/fixtures/vector_tile.js index 5796828..ca9e9c8 100644 --- a/test/fixtures/vector_tile.js +++ b/test/fixtures/vector_tile.js @@ -1,14 +1,39 @@ +/** + * @typedef {import("../../index.js").default} Pbf + */ +/** + * @typedef {object} Tile + * @property {Array} layers + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {Tile} + */ export function readTile(pbf, end) { return pbf.readFields(readTileField, {layers: []}, end); } + +/** + * @param {number} tag + * @param {Tile} obj + * @param {Pbf} pbf + */ function readTileField(tag, obj, pbf) { if (tag === 3) obj.layers.push(readTileLayer(pbf, pbf.readVarint() + pbf.pos)); } + +/** + * @param {Tile} obj + * @param {Pbf} pbf + */ export function writeTile(obj, pbf) { if (obj.layers) for (const item of obj.layers) pbf.writeMessage(3, writeTileLayer, item); } +/** @enum {number} */ export const TileGeomType = { "UNKNOWN": 0, "POINT": 1, @@ -16,9 +41,31 @@ export const TileGeomType = { "POLYGON": 3 }; +/** + * @typedef {object} TileValue + * @property {string} [string_value] + * @property {number} [float_value] + * @property {number} [double_value] + * @property {number} [int_value] + * @property {number} [uint_value] + * @property {number} [sint_value] + * @property {boolean} [bool_value] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {TileValue} + */ export function readTileValue(pbf, end) { return pbf.readFields(readTileValueField, {string_value: "", float_value: 0, double_value: 0, int_value: 0, uint_value: 0, sint_value: 0, bool_value: false}, end); } + +/** + * @param {number} tag + * @param {TileValue} obj + * @param {Pbf} pbf + */ function readTileValueField(tag, obj, pbf) { if (tag === 1) obj.string_value = pbf.readString(); else if (tag === 2) obj.float_value = pbf.readFloat(); @@ -28,6 +75,11 @@ function readTileValueField(tag, obj, pbf) { else if (tag === 6) obj.sint_value = pbf.readSVarint(); else if (tag === 7) obj.bool_value = pbf.readBoolean(); } + +/** + * @param {TileValue} obj + * @param {Pbf} pbf + */ export function writeTileValue(obj, pbf) { if (obj.string_value) pbf.writeStringField(1, obj.string_value); if (obj.float_value) pbf.writeFloatField(2, obj.float_value); @@ -38,15 +90,39 @@ export function writeTileValue(obj, pbf) { if (obj.bool_value) pbf.writeBooleanField(7, obj.bool_value); } +/** + * @typedef {object} TileFeature + * @property {number} [id] + * @property {Array} tags + * @property {TileGeomType} [type] + * @property {Array} geometry + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {TileFeature} + */ export function readTileFeature(pbf, end) { return pbf.readFields(readTileFeatureField, {id: 0, tags: [], type: 0, geometry: []}, end); } + +/** + * @param {number} tag + * @param {TileFeature} obj + * @param {Pbf} pbf + */ function readTileFeatureField(tag, obj, pbf) { if (tag === 1) obj.id = pbf.readVarint(); else if (tag === 2) pbf.readPackedVarint(obj.tags); else if (tag === 3) obj.type = pbf.readVarint(); else if (tag === 4) pbf.readPackedVarint(obj.geometry); } + +/** + * @param {TileFeature} obj + * @param {Pbf} pbf + */ export function writeTileFeature(obj, pbf) { if (obj.id) pbf.writeVarintField(1, obj.id); if (obj.tags) pbf.writePackedVarint(2, obj.tags); @@ -54,9 +130,30 @@ export function writeTileFeature(obj, pbf) { if (obj.geometry) pbf.writePackedVarint(4, obj.geometry); } +/** + * @typedef {object} TileLayer + * @property {number} version + * @property {string} name + * @property {Array} features + * @property {Array} keys + * @property {Array} values + * @property {number} [extent] + */ + +/** + * @param {Pbf} pbf + * @param {number} [end] + * @returns {TileLayer} + */ export function readTileLayer(pbf, end) { return pbf.readFields(readTileLayerField, {version: 1, name: "", features: [], keys: [], values: [], extent: 4096}, end); } + +/** + * @param {number} tag + * @param {TileLayer} obj + * @param {Pbf} pbf + */ function readTileLayerField(tag, obj, pbf) { if (tag === 15) obj.version = pbf.readVarint(); else if (tag === 1) obj.name = pbf.readString(); @@ -65,6 +162,11 @@ function readTileLayerField(tag, obj, pbf) { else if (tag === 4) obj.values.push(readTileValue(pbf, pbf.readVarint() + pbf.pos)); else if (tag === 5) obj.extent = pbf.readVarint(); } + +/** + * @param {TileLayer} obj + * @param {Pbf} pbf + */ export function writeTileLayer(obj, pbf) { if (obj.version != null && obj.version !== 1) pbf.writeVarintField(15, obj.version); if (obj.name) pbf.writeStringField(1, obj.name); diff --git a/test/typescript/defaults.ts b/test/typescript/defaults.ts new file mode 100644 index 0000000..fe05384 --- /dev/null +++ b/test/typescript/defaults.ts @@ -0,0 +1,28 @@ +import Pbf from 'pbf'; + +import {readEnvelope, writeEnvelope, MessageType} from '../fixtures/defaults.js' +import type {Envelope} from '../fixtures/defaults.js' + +const pbf = new Pbf(); +readEnvelope(pbf) satisfies Envelope; + +const valid: Envelope = { + type: MessageType.GREETING, + name: 'test', + flag: true, + weight: 1.5, + id: 1, +}; + +const invalid = { + type: null, + name: null, + flag: null, + weight: null, + id: null, +}; + +writeEnvelope(valid, pbf); + +// @ts-expect-error +writeEnvelope(invalid, pbf); diff --git a/test/typescript/defaults_implicit.ts b/test/typescript/defaults_implicit.ts new file mode 100644 index 0000000..ddcca98 --- /dev/null +++ b/test/typescript/defaults_implicit.ts @@ -0,0 +1,38 @@ +import Pbf from 'pbf'; + +import {readEnvelope, writeEnvelope, MessageType} from '../fixtures/defaults_implicit.js' +import type {Envelope, CustomType} from '../fixtures/defaults_implicit.js' + +const pbf = new Pbf(); +readEnvelope(pbf) satisfies Envelope; + +const valid: Envelope = { + type: MessageType.GREETING, + name: 'test', + flag: true, + weight: 1.5, + id: 1, + tags: [''], + numbers: [0], + bytes: new Uint8Array(0), + custom: {} satisfies CustomType, + types: [MessageType.GREETING], +}; + +const invalid = { + type: null, + name: null, + flag: null, + weight: null, + id: null, + tags: null, + numbers: null, + bytes: null, + custom: null, + types: null, +}; + +writeEnvelope(valid, pbf); + +// @ts-expect-error +writeEnvelope(invalid, pbf); diff --git a/test/typescript/package.json b/test/typescript/package.json new file mode 100644 index 0000000..07aaeb6 --- /dev/null +++ b/test/typescript/package.json @@ -0,0 +1,14 @@ +{ + "name": "pbf-ts", + "private": true, + "type": "module", + "scripts": { + "tsc": "tsc --project tsconfig.json" + }, + "dependencies": { + "pbf": "file:../../" + }, + "devDependencies": { + "typescript": "^5.5.3" + } +} diff --git a/test/typescript/tsconfig.json b/test/typescript/tsconfig.json new file mode 100644 index 0000000..abb9a40 --- /dev/null +++ b/test/typescript/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "checkJs": true, + "emitDeclarationOnly": false, + "noEmit": true, + }, + "include": ["*.ts"], +}