Skip to content

Commit

Permalink
jsdoc support
Browse files Browse the repository at this point in the history
  • Loading branch information
stepankuzmin committed Aug 20, 2024
1 parent a9d0a8d commit 4c61a1a
Show file tree
Hide file tree
Showing 18 changed files with 813 additions and 25 deletions.
3 changes: 2 additions & 1 deletion bin/pbf
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

Expand Down
160 changes: 138 additions & 22 deletions compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -24,17 +30,35 @@ function writeContext(ctx, options) {
}

function writeMessage(ctx, options) {
const name = ctx._name;
const fields = ctx._proto.fields;

let code = '\n';

if (options.jsDoc) {
code += compileType(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) {
let param = name;
if (ctx._proto.map) {
const {key, value} = getMapTsType(fields);
param = `{key: ${key}; value: ${value}}`;
}
code += ['\n/**', ' * @param {number} tag', ` * @param {${param}} 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];
Expand All @@ -60,7 +84,18 @@ function ${readName}Field(tag, obj, pbf) {
}

if (!options.noWrite) {
const writeName = `write${ctx._name}`;
const writeName = `write${name}`;

if (options.jsDoc) {
let param = name;
if (ctx._proto.map) {
const {key, value} = getMapTsType(fields);
param = `{key: ${key}; value: ${value}}`;
}

code += ['\n/**', ` * @param {${param}} obj`, ' * @param {Pbf} pbf', ' */'].join('\n').concat('\n');
}

code += `${writeFunctionExport(options, writeName)}function ${writeName}(obj, pbf) {\n`;
for (const field of fields) {
const writeCode =
Expand All @@ -87,10 +122,17 @@ function getEnumValues(ctx) {
return enums;
}

function writeEnum(ctx, {legacy}) {
function writeEnum(ctx, options) {
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 code = '\n';
if (options.jsDoc) {
code = '\n/** @enum {number} */\n';
}

code += `${options.legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`;
return code;
}

function compileDest(ctx) {
Expand All @@ -114,23 +156,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;
}
Expand Down Expand Up @@ -256,6 +302,7 @@ function getMapMessage(field) {
return {
name: getMapMessageName(field.tag),
enums: [],
map: true,
messages: [],
extensions: null,
fields: [
Expand Down Expand Up @@ -415,3 +462,72 @@ function getDefaultWriteTest(ctx, field) {
function isPacked(field) {
return field.options.packed === 'true';
}

function getTsType(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;
}

function getMapTsType(fields) {
const key = getTsType(fields[0]);
const value = getTsType(fields[1]);
return {key, value};
}

/**
* @param {string} type
* @param {string} name
* @param {{name: string; type: string; required: boolean}} [fields]
* @returns {string}
*/
function typeDef(type, name, fields = []) {
const unionProperties = {};
const properties = fields.map((field) => {
if (field.oneof) {
unionProperties[field.oneof] = unionProperties[field.oneof] || [];
unionProperties[field.oneof].push(field.name);
}

const type = getTsType(field);
const isRequired = field.required || field.repeated || field.map;

const name = isRequired ? field.name : `[${field.name}]`;

return ` * @property {${type}} ${name}`;
});

for (const prop in unionProperties) {
const union = unionProperties[prop].map(s => `${name}["${s}"]`).join(' | ');
properties.push(` * @property {${union}} ${prop}`);
}

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 compileType(ctx, name, fields = []) {
if (ctx._proto.map) {
const {key, value} = getMapTsType(fields);
return typeDef(`Object<${key}, ${value}>`, name, []);
}

const typedFields = fields.map((field) => {
const type = getType(ctx, field);
return {...field, type: type ? type._name : field.type};
});

return typeDef('object', name, typedFields);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 --project tsconfig.test.json",
"cov": "node --test --experimental-test-covetage",
"build": "rollup -c",
"prepublishOnly": "npm run test && npm run build"
Expand Down
2 changes: 1 addition & 1 deletion test/compile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('compiles all proto files to proper js', () => {
for (const path of files) {
if (!path.endsWith('.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);
Expand Down
29 changes: 29 additions & 0 deletions test/fixtures/defaults.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
/**
* @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();
else if (tag === 3) obj.flag = pbf.readBoolean();
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);
Expand Down
54 changes: 54 additions & 0 deletions test/fixtures/defaults_implicit.js
Original file line number Diff line number Diff line change
@@ -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<string>} tags
* @property {Array<number>} numbers
* @property {Uint8Array} [bytes]
* @property {CustomType} [custom]
* @property {Array<MessageType>} 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();
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 4c61a1a

Please sign in to comment.