418 lines
12 KiB
JavaScript
418 lines
12 KiB
JavaScript
|
|
import {readFileSync} from 'fs';
|
|
|
|
const version = JSON.parse(readFileSync(new URL('package.json', import.meta.url))).version;
|
|
|
|
export function compile(proto) {
|
|
return new Function(`const exports = {};\n${compileRaw(proto, {legacy: true})}\nreturn exports;`)();
|
|
}
|
|
|
|
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)}`;
|
|
}
|
|
|
|
function writeContext(ctx, options) {
|
|
let code = '';
|
|
if (ctx._proto.fields) code += writeMessage(ctx, options);
|
|
if (ctx._proto.values) code += writeEnum(ctx, options);
|
|
|
|
for (let i = 0; i < ctx._children.length; i++) {
|
|
code += writeContext(ctx._children[i], options);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
function writeMessage(ctx, options) {
|
|
const fields = ctx._proto.fields;
|
|
|
|
let code = '\n';
|
|
|
|
if (!options.noRead) {
|
|
const readName = `read${ctx._name}`;
|
|
code +=
|
|
`${writeFunctionExport(options, readName)}function ${readName}(pbf, end) {
|
|
return pbf.readFields(${readName}Field, ${compileDest(ctx)}, end);
|
|
}
|
|
function ${readName}Field(tag, obj, pbf) {
|
|
`;
|
|
for (let i = 0; i < fields.length; i++) {
|
|
const field = fields[i];
|
|
const {type, name, repeated, oneof, tag} = field;
|
|
const readCode = compileFieldRead(ctx, field);
|
|
const packed = willSupportPacked(ctx, field);
|
|
|
|
let fieldRead =
|
|
type === 'map' ? compileMapRead(readCode, name) :
|
|
repeated ? (packed ? readCode : `obj.${name}.push(${readCode})`) :
|
|
`obj.${name} = ${readCode}`;
|
|
|
|
if (oneof) {
|
|
fieldRead += `; obj.${oneof} = ${JSON.stringify(name)}`;
|
|
}
|
|
|
|
fieldRead = type === 'map' || oneof ? `{ ${fieldRead}; }` : `${fieldRead};`;
|
|
|
|
code +=
|
|
` ${i ? 'else ' : ''}if (tag === ${tag}) ${fieldRead}\n`;
|
|
}
|
|
code += '}\n';
|
|
}
|
|
|
|
if (!options.noWrite) {
|
|
const writeName = `write${ctx._name}`;
|
|
code += `${writeFunctionExport(options, writeName)}function ${writeName}(obj, pbf) {\n`;
|
|
for (const field of fields) {
|
|
const writeCode =
|
|
field.repeated && !isPacked(field) ? compileRepeatedWrite(ctx, field) :
|
|
field.type === 'map' ? compileMapWrite(ctx, field) : compileFieldWrite(ctx, field, `obj.${field.name}`);
|
|
code += getDefaultWriteTest(ctx, field);
|
|
code += `${writeCode};\n`;
|
|
}
|
|
code += '}\n';
|
|
}
|
|
return code;
|
|
}
|
|
|
|
function writeFunctionExport({legacy}, name) {
|
|
return legacy ? `exports.${name} = ${name};\n` : 'export ';
|
|
}
|
|
|
|
function getEnumValues(ctx) {
|
|
const enums = {};
|
|
const ids = Object.keys(ctx._proto.values);
|
|
for (let i = 0; i < ids.length; i++) {
|
|
enums[ids[i]] = ctx._proto.values[ids[i]].value;
|
|
}
|
|
return enums;
|
|
}
|
|
|
|
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`;
|
|
}
|
|
|
|
function compileDest(ctx) {
|
|
const props = new Set();
|
|
for (const {name, oneof} of ctx._proto.fields) {
|
|
props.add(`${name}: ${JSON.stringify(ctx._defaults[name])}`);
|
|
if (oneof) props.add(`${oneof }: undefined`);
|
|
}
|
|
return `{${[...props].join(', ')}}`;
|
|
}
|
|
|
|
function isEnum(type) {
|
|
return type && type._proto.values;
|
|
}
|
|
|
|
function getType(ctx, field) {
|
|
if (field.type === 'map') {
|
|
return ctx[getMapMessageName(field.tag)];
|
|
}
|
|
const path = field.type.split('.');
|
|
return path.reduce((ctx, name) => ctx && ctx[name], ctx);
|
|
}
|
|
|
|
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 false;
|
|
}
|
|
|
|
function compileFieldRead(ctx, field) {
|
|
const type = getType(ctx, field);
|
|
if (type) {
|
|
if (type._proto.fields) return `read${type._name}(pbf, pbf.readVarint() + pbf.pos)`;
|
|
if (!isEnum(type)) throw new Error(`Unexpected type: ${type._name}`);
|
|
}
|
|
|
|
const fieldType = isEnum(type) ? 'enum' : field.type;
|
|
|
|
let prefix = 'pbf.read';
|
|
const signed = fieldType === 'int32' || fieldType === 'int64' ? 'true' : '';
|
|
let suffix = `(${signed})`;
|
|
|
|
if (willSupportPacked(ctx, field)) {
|
|
prefix += 'Packed';
|
|
suffix = `(obj.${field.name}${signed ? `, ${signed}` : ''})`;
|
|
}
|
|
|
|
if (fieldShouldUseStringAsNumber(field)) {
|
|
suffix += '.toString()';
|
|
}
|
|
|
|
switch (fieldType) {
|
|
case 'string': return `${prefix}String${suffix}`;
|
|
case 'float': return `${prefix}Float${suffix}`;
|
|
case 'double': return `${prefix}Double${suffix}`;
|
|
case 'bool': return `${prefix}Boolean${suffix}`;
|
|
case 'enum':
|
|
case 'uint32':
|
|
case 'uint64':
|
|
case 'int32':
|
|
case 'int64': return `${prefix}Varint${suffix}`;
|
|
case 'sint32':
|
|
case 'sint64': return `${prefix}SVarint${suffix}`;
|
|
case 'fixed32': return `${prefix}Fixed32${suffix}`;
|
|
case 'fixed64': return `${prefix}Fixed64${suffix}`;
|
|
case 'sfixed32': return `${prefix}SFixed32${suffix}`;
|
|
case 'sfixed64': return `${prefix}SFixed64${suffix}`;
|
|
case 'bytes': return `${prefix}Bytes${suffix}`;
|
|
default: throw new Error(`Unexpected type: ${field.type}`);
|
|
}
|
|
}
|
|
|
|
function compileFieldWrite(ctx, field, name) {
|
|
let prefix = 'pbf.write';
|
|
if (isPacked(field)) prefix += 'Packed';
|
|
|
|
if (fieldShouldUseStringAsNumber(field)) {
|
|
if (field.type === 'float' || field.type === 'double') {
|
|
name = `parseFloat(${name})`;
|
|
} else {
|
|
name = `parseInt(${name}, 10)`;
|
|
}
|
|
}
|
|
const postfix = `${isPacked(field) ? '' : 'Field'}(${field.tag}, ${name})`;
|
|
|
|
const type = getType(ctx, field);
|
|
if (type) {
|
|
if (type._proto.fields) return `${prefix}Message(${field.tag}, write${type._name}, ${name})`;
|
|
if (type._proto.values) return `${prefix}Varint${postfix}`;
|
|
throw new Error(`Unexpected type: ${type._name}`);
|
|
}
|
|
|
|
switch (field.type) {
|
|
case 'string': return `${prefix}String${postfix}`;
|
|
case 'float': return `${prefix}Float${postfix}`;
|
|
case 'double': return `${prefix}Double${postfix}`;
|
|
case 'bool': return `${prefix}Boolean${postfix}`;
|
|
case 'enum':
|
|
case 'uint32':
|
|
case 'uint64':
|
|
case 'int32':
|
|
case 'int64': return `${prefix}Varint${postfix}`;
|
|
case 'sint32':
|
|
case 'sint64': return `${prefix}SVarint${postfix}`;
|
|
case 'fixed32': return `${prefix}Fixed32${postfix}`;
|
|
case 'fixed64': return `${prefix}Fixed64${postfix}`;
|
|
case 'sfixed32': return `${prefix}SFixed32${postfix}`;
|
|
case 'sfixed64': return `${prefix}SFixed64${postfix}`;
|
|
case 'bytes': return `${prefix}Bytes${postfix}`;
|
|
default: throw new Error(`Unexpected type: ${field.type}`);
|
|
}
|
|
}
|
|
|
|
function compileMapRead(readCode, name) {
|
|
return `const {key, value} = ${readCode}; obj.${name}[key] = value`;
|
|
}
|
|
|
|
function compileRepeatedWrite(ctx, field) {
|
|
return `for (const item of obj.${field.name}) ${
|
|
compileFieldWrite(ctx, field, 'item')}`;
|
|
}
|
|
|
|
function compileMapWrite(ctx, field) {
|
|
const name = `obj.${field.name}`;
|
|
|
|
return `for (const key of Object.keys(${name})) ${
|
|
compileFieldWrite(ctx, field, `{key, value: ${name}[key]}`)}`;
|
|
}
|
|
|
|
function getMapMessageName(tag) {
|
|
return `_FieldEntry${tag}`;
|
|
}
|
|
|
|
function getMapField(name, type, tag) {
|
|
return {
|
|
name,
|
|
type,
|
|
tag,
|
|
map: null,
|
|
oneof: null,
|
|
required: false,
|
|
repeated: false,
|
|
options: {}
|
|
};
|
|
}
|
|
|
|
function getMapMessage(field) {
|
|
return {
|
|
name: getMapMessageName(field.tag),
|
|
enums: [],
|
|
messages: [],
|
|
extensions: null,
|
|
fields: [
|
|
getMapField('key', field.map.from, 1),
|
|
getMapField('value', field.map.to, 2)
|
|
]
|
|
};
|
|
}
|
|
|
|
function buildContext(proto, parent) {
|
|
const obj = Object.create(parent);
|
|
obj._proto = proto;
|
|
obj._children = [];
|
|
obj._defaults = {};
|
|
|
|
if (parent) {
|
|
parent[proto.name] = obj;
|
|
|
|
if (parent._name) {
|
|
obj._name = parent._name + proto.name;
|
|
} else {
|
|
obj._name = proto.name;
|
|
}
|
|
}
|
|
|
|
for (let i = 0; proto.enums && i < proto.enums.length; i++) {
|
|
obj._children.push(buildContext(proto.enums[i], obj));
|
|
}
|
|
|
|
for (let i = 0; proto.messages && i < proto.messages.length; i++) {
|
|
obj._children.push(buildContext(proto.messages[i], obj));
|
|
}
|
|
|
|
for (let i = 0; proto.fields && i < proto.fields.length; i++) {
|
|
if (proto.fields[i].type === 'map') {
|
|
obj._children.push(buildContext(getMapMessage(proto.fields[i]), obj));
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
function getDefaultValue(field, value) {
|
|
// Defaults not supported for repeated fields
|
|
if (field.repeated) return [];
|
|
let convertToStringIfNeeded = function (val) { return val; };
|
|
if (fieldShouldUseStringAsNumber(field)) {
|
|
convertToStringIfNeeded = function (val) { return val.toString(); };
|
|
}
|
|
|
|
switch (field.type) {
|
|
case 'float':
|
|
case 'double': return convertToStringIfNeeded(value ? parseFloat(value) : 0);
|
|
case 'uint32':
|
|
case 'uint64':
|
|
case 'int32':
|
|
case 'int64':
|
|
case 'sint32':
|
|
case 'sint64':
|
|
case 'fixed32':
|
|
case 'fixed64':
|
|
case 'sfixed32':
|
|
case 'sfixed64': return convertToStringIfNeeded(value ? parseInt(value, 10) : 0);
|
|
case 'string': return value || '';
|
|
case 'bool': return value === 'true';
|
|
case 'map': return {};
|
|
default: return undefined;
|
|
}
|
|
}
|
|
|
|
function willSupportPacked(ctx, field) {
|
|
const fieldType = isEnum(getType(ctx, field)) ? 'enum' : field.type;
|
|
|
|
switch (field.repeated && fieldType) {
|
|
case 'float':
|
|
case 'double':
|
|
case 'uint32':
|
|
case 'uint64':
|
|
case 'int32':
|
|
case 'int64':
|
|
case 'sint32':
|
|
case 'sint64':
|
|
case 'fixed32':
|
|
case 'fixed64':
|
|
case 'sfixed32':
|
|
case 'enum':
|
|
case 'bool': return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function setPackedOption(ctx, field, syntax) {
|
|
// No default packed in older protobuf versions
|
|
if (syntax < 3) return;
|
|
|
|
// Packed option already set
|
|
if (field.options.packed !== undefined) return;
|
|
|
|
// Not a packed field type
|
|
if (!willSupportPacked(ctx, field)) return;
|
|
|
|
field.options.packed = 'true';
|
|
}
|
|
|
|
function setDefaultValue(ctx, field, syntax) {
|
|
const options = field.options;
|
|
const type = getType(ctx, field);
|
|
const enumValues = type && type._proto.values && getEnumValues(type);
|
|
|
|
// Proto3 does not support overriding defaults
|
|
const explicitDefault = syntax < 3 ? options.default : undefined;
|
|
|
|
// Set default for enum values
|
|
if (enumValues && !field.repeated) {
|
|
ctx._defaults[field.name] = enumValues[explicitDefault] || 0;
|
|
|
|
} else {
|
|
ctx._defaults[field.name] = getDefaultValue(field, explicitDefault);
|
|
}
|
|
}
|
|
|
|
function buildDefaults(ctx, syntax) {
|
|
const proto = ctx._proto;
|
|
|
|
for (let i = 0; i < ctx._children.length; i++) {
|
|
buildDefaults(ctx._children[i], syntax);
|
|
}
|
|
|
|
if (proto.fields) {
|
|
for (let i = 0; i < proto.fields.length; i++) {
|
|
setPackedOption(ctx, proto.fields[i], syntax);
|
|
setDefaultValue(ctx, proto.fields[i], syntax);
|
|
}
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
|
|
function getDefaultWriteTest(ctx, field) {
|
|
const def = ctx._defaults[field.name];
|
|
const type = getType(ctx, field);
|
|
let code = ` if (obj.${field.name}`;
|
|
|
|
if (!field.repeated && (!type || !type._proto.fields)) {
|
|
if (def === undefined || def || field.oneof) {
|
|
code += ' != null';
|
|
}
|
|
if (def) {
|
|
code += ` && obj.${field.name} !== ${JSON.stringify(def)}`;
|
|
}
|
|
}
|
|
|
|
return `${code}) `;
|
|
}
|
|
|
|
function isPacked(field) {
|
|
return field.options.packed === 'true';
|
|
}
|