pg-proto-parser
Version:
The LaunchQL Proto parser
305 lines (304 loc) • 13.7 kB
JavaScript
import { Service, Type, Enum, Namespace } from '@launchql/protobufjs';
import { generateEnumImports, generateAstHelperMethods, generateWrappedAstHelperMethods, generateTypeImportSpecifiers, generateEnumValueFunctions, generateEnumToIntFunctions, generateEnumToStringFunctions, generateEnumToIntFunctionsNested, generateEnumToStringFunctionsNested, convertEnumToTsUnionType, convertEnumToTsEnumDeclaration, generateNodeUnionType, convertTypeToTsInterface } from './ast';
import { RuntimeSchemaGenerator } from './runtime-schema';
import { generateEnum2IntJSON, generateEnum2StrJSON } from './ast/enums/enums-json';
import { jsStringify } from 'strfy-js';
import { mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { getOptionsWithDefaults } from './options';
import { cloneAndNameNode, convertAstToCode, createDefaultImport, getUndefinedKey, hasUndefinedInitialValue, stripExtension, writeFileToDisk } from './utils';
import { nestedObjCode } from './inline-helpers';
import { NODE_TYPE } from './constants';
export class ProtoStore {
options;
root;
services;
types;
fields;
enums;
namespaces;
_runtimeSchema;
constructor(root, options = {}) {
this.options = getOptionsWithDefaults(options);
this.root = root;
this.services = [];
this.types = [];
this.fields = [];
this.enums = [];
this.namespaces = [];
this._parse(this.root);
}
_parse(node, name = '') {
if (node instanceof Service) {
this.services.push(cloneAndNameNode(node, name));
}
else if (node instanceof Type) {
this.types.push(cloneAndNameNode(node, name));
node.fieldsArray.forEach(field => this.fields.push(field));
}
else if (node instanceof Enum) {
this.enums.push(cloneAndNameNode(this._processEnum(node), name));
}
if (node instanceof Namespace) {
this.namespaces.push(node);
Object.entries(node.nested || {}).forEach(([key, child]) => {
this._parse(child, key);
});
}
}
_processEnum(enumNode) {
const undefinedKey = getUndefinedKey(enumNode.name);
const clone = cloneAndNameNode(enumNode, enumNode.name);
if (!this.options.enums.removeUndefinedAt0 || !hasUndefinedInitialValue(enumNode)) {
return clone;
}
const newValues = {};
let decrement = 0;
for (const [key, value] of Object.entries(enumNode.values)) {
if (key === undefinedKey && value === 0) {
decrement = 1;
continue;
}
newValues[key] = value - decrement;
}
clone.values = newValues;
return clone;
}
write() {
// Ensure the output directory exists
mkdirSync(this.options.outDir, { recursive: true });
this.writeEnumMaps();
this.writeTypes();
this.writeEnums();
this.writeUtilsEnums();
this.writeAstHelpers();
this.writeWrappedAstHelpers();
this.writeRuntimeSchema();
}
writeEnumMaps() {
if (!this.options.enums.enumMap?.enabled) {
return;
}
const enumsToProcess = this.enumsToProcess();
const enums2int = generateEnum2IntJSON(enumsToProcess);
const enums2str = generateEnum2StrJSON(enumsToProcess);
const format = this.options.enums.enumMap.format || 'json';
if (format === 'json') {
// Write plain JSON files
if (this.options.enums.enumMap.toIntOutFile) {
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toIntOutFile, '.json');
this.writeFile(filename, JSON.stringify(enums2int, null, 2));
}
if (this.options.enums.enumMap.toStrOutFile) {
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toStrOutFile, '.json');
this.writeFile(filename, JSON.stringify(enums2str, null, 2));
}
}
else if (format === 'ts') {
// Write TypeScript files with exports
if (this.options.enums.enumMap.toIntOutFile) {
const tsContent = this.generateEnumMapTypeScript(enums2int, 'enumToIntMap', 'EnumToIntMap');
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toIntOutFile, '.ts');
this.writeFile(filename, tsContent);
}
if (this.options.enums.enumMap.toStrOutFile) {
const tsContent = this.generateEnumMapTypeScript(enums2str, 'enumToStrMap', 'EnumToStrMap');
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toStrOutFile, '.ts');
this.writeFile(filename, tsContent);
}
}
}
allTypesExceptNode() {
return this.types.filter(type => type.name !== NODE_TYPE);
}
typesToProcess() {
return this.types
.filter(type => type.name !== NODE_TYPE)
.filter(type => !this.options.exclude.includes(type.name));
}
enumsToProcess() {
return this.enums
.filter(enm => !this.options.exclude.includes(enm.name));
}
writeTypes() {
if (this.options.types.enabled) {
const typesToProcess = this.typesToProcess();
const enumsToProcess = this.enumsToProcess();
const node = generateNodeUnionType(this.options, typesToProcess);
const enumImports = generateEnumImports(enumsToProcess, this.options.types.enumsSource);
const types = typesToProcess.reduce((m, type) => {
return [...m, convertTypeToTsInterface(type, this.options)];
}, []);
const filename = this.ensureCorrectExtension(this.options.types.filename, '.ts');
this.writeCodeToFile(filename, [
enumImports,
node,
...types
]);
}
}
writeEnums() {
if (this.options.enums.enabled) {
const filename = this.ensureCorrectExtension(this.options.enums.filename, '.ts');
this.writeCodeToFile(filename, this.enumsToProcess().map(enm => this.options.enums.enumsAsTypeUnion ?
convertEnumToTsUnionType(enm) :
convertEnumToTsEnumDeclaration(enm)));
}
}
writeUtilsEnums() {
if (this.options.utils.enums.enabled) {
const enumsToProcess = this.enumsToProcess();
const useNestedObjects = this.options.utils.enums.outputFormat === 'nestedObjects';
if (this.options.utils.enums.unidirectional) {
// Generate separate unidirectional functions
const toIntGenerator = useNestedObjects ? generateEnumToIntFunctionsNested : generateEnumToIntFunctions;
const toStringGenerator = useNestedObjects ? generateEnumToStringFunctionsNested : generateEnumToStringFunctions;
const toIntCode = convertAstToCode(toIntGenerator(enumsToProcess));
const toIntFilename = this.ensureCorrectExtension(this.options.utils.enums.toIntFilename, '.ts');
this.writeFile(toIntFilename, toIntCode);
const toStringCode = convertAstToCode(toStringGenerator(enumsToProcess));
const toStringFilename = this.ensureCorrectExtension(this.options.utils.enums.toStringFilename, '.ts');
this.writeFile(toStringFilename, toStringCode);
}
else {
// Generate bidirectional function (original behavior)
// Note: Nested objects format only supported for unidirectional functions
const code = convertAstToCode(generateEnumValueFunctions(enumsToProcess));
const filename = this.ensureCorrectExtension(this.options.utils.enums.filename, '.ts');
this.writeFile(filename, code);
}
}
}
writeAstHelpers() {
if (this.options.utils.astHelpers.enabled) {
const imports = this.options.utils.astHelpers.inlineNestedObj ?
createDefaultImport('_o', './' + stripExtension(this.options.utils.astHelpers.nestedObjFile)) :
createDefaultImport('_o', 'nested-obj');
const typesToProcess = this.typesToProcess();
const code = convertAstToCode([
imports,
generateTypeImportSpecifiers(typesToProcess, this.options),
generateAstHelperMethods(typesToProcess)
]);
const filename = this.ensureCorrectExtension(this.options.utils.astHelpers.filename, '.ts');
this.writeFile(filename, code);
if (this.options.utils.astHelpers.inlineNestedObj) {
const nestedObjFilename = this.ensureCorrectExtension(this.options.utils.astHelpers.nestedObjFile, '.ts');
this.writeFile(nestedObjFilename, nestedObjCode);
}
}
}
writeWrappedAstHelpers() {
if (this.options.utils.wrappedAstHelpers?.enabled) {
const imports = createDefaultImport('_o', 'nested-obj');
const typesToProcess = this.typesToProcess();
const code = convertAstToCode([
imports,
generateTypeImportSpecifiers(typesToProcess, this.options),
generateWrappedAstHelperMethods(typesToProcess)
]);
const filename = this.ensureCorrectExtension(this.options.utils.wrappedAstHelpers.filename, '.ts');
this.writeFile(filename, code);
}
}
writeRuntimeSchema() {
if (!this.options.runtimeSchema?.enabled) {
return;
}
const generator = new RuntimeSchemaGenerator(this.root);
const nodeSpecs = generator.generateNodeSpecs();
const format = this.options.runtimeSchema.format || 'json';
const filename = this.options.runtimeSchema.filename || 'runtime-schema';
if (format === 'json') {
const jsonContent = JSON.stringify(nodeSpecs, null, 2);
const correctedFilename = this.ensureCorrectExtension(filename, '.json');
const outFile = join(this.options.outDir, correctedFilename);
writeFileToDisk(outFile, jsonContent, this.options);
}
else if (format === 'typescript') {
const tsContent = this.generateRuntimeSchemaTypeScript(nodeSpecs);
const correctedFilename = this.ensureCorrectExtension(filename, '.ts');
const outFile = join(this.options.outDir, correctedFilename);
writeFileToDisk(outFile, tsContent, this.options);
}
}
getRuntimeSchema() {
if (!this._runtimeSchema) {
const generator = new RuntimeSchemaGenerator(this.root);
this._runtimeSchema = generator.generateNodeSpecs();
}
return this._runtimeSchema;
}
generateRuntimeSchemaTypeScript(nodeSpecs) {
const interfaceDefinitions = [
'export interface FieldSpec {',
' name: string;',
' type: string;',
' isArray: boolean;',
' optional: boolean;',
'}',
'',
'export interface NodeSpec {',
' name: string;',
' isNode: boolean;',
' fields: FieldSpec[];',
'}',
''
];
const exportStatement = `export const runtimeSchema: NodeSpec[] = ${jsStringify(nodeSpecs, {
space: 2,
camelCase: true,
quotes: 'single'
})};`;
return [
...interfaceDefinitions,
exportStatement
].filter(Boolean).join('\n');
}
generateEnumMapTypeScript(enumMap, varName, typeName) {
const exportStatement = `export const ${varName} = ${jsStringify(enumMap, {
space: 2,
camelCase: false, // Preserve enum casing
quotes: 'single'
})};`;
const typeStatement = `export type ${typeName} = typeof ${varName};`;
return [
exportStatement,
'',
typeStatement
].filter(Boolean).join('\n');
}
ensureCorrectExtension(filename, expectedExt) {
if (!filename || !expectedExt) {
return filename || '';
}
// Ensure expectedExt starts with a dot
if (!expectedExt.startsWith('.')) {
expectedExt = '.' + expectedExt;
}
const extMatch = filename.match(/(\.[^./\\]+)+$/);
const currentExt = extMatch ? extMatch[0] : '';
if (currentExt && currentExt !== expectedExt) {
// Replace the current extension with the expected one
return filename.slice(0, -currentExt.length) + expectedExt;
}
else if (!currentExt) {
// No extension, add the expected one
return filename + expectedExt;
}
// Extension is already correct
return filename;
}
writeFile(filePath, content) {
const fullPath = join(this.options.outDir, filePath);
const dir = dirname(fullPath);
mkdirSync(dir, { recursive: true });
writeFileToDisk(fullPath, content, this.options);
}
writeCodeToFile(filename, nodes) {
const code = convertAstToCode(nodes);
const correctedFilename = this.ensureCorrectExtension(filename, '.ts');
const filePath = join(this.options.outDir, correctedFilename);
writeFileToDisk(filePath, code, this.options);
}
}