fileserver/tools/apigen.ts

248 lines
6.4 KiB
TypeScript

import {
AST_NODE_TYPES,
createProgram,
parse
} from '@typescript-eslint/typescript-estree';
import * as fs from 'fs';
import * as path from 'path';
import * as a from '@typescript-eslint/types/dist/generated/ast-spec';
import { TypeNode } from '@typescript-eslint/types/dist/generated/ast-spec';
import { Writable } from 'stream';
class Err extends Error {
constructor(o: object) {
super();
console.error(o);
}
}
const program = createProgram('tsconfig.json');
class BetterWritable {
constructor(private w: Writable) {}
private indent = 0;
write(data: string) {
this.w.write('\t'.repeat(this.indent));
this.w.write(data);
this.w.write('\n');
}
writeInc(data: string) {
this.write(data);
this.indent++;
}
writeDec(data: string) {
this.indent--;
this.write(data);
}
}
interface Outputable {
write(s: BetterWritable);
}
class CppProp {
constructor(entry: string) {
this.entry = entry;
}
entry: string;
}
class CppClass implements Outputable {
constructor(name: string) {
this.name = name;
}
name: string;
sub: string[] = [];
entries: CppProp[] = [];
write(s: BetterWritable) {
let name = `struct ${this.name}`;
if (this.sub.length > 0) name += ' : ' + this.sub.join(', ');
if (this.entries.length == 0) s.write(`${name} {}`);
else {
s.writeInc(`${name} {`);
this.entries.forEach((e) => s.write(`${e.entry};`));
s.writeDec('};');
}
}
}
class CppEnum implements Outputable {
constructor(name: string) {
this.name = name;
}
name: string;
entries: CppProp[] = [];
write(s: BetterWritable) {
s.writeInc(`enum ${this.name} {`);
this.entries.forEach((e, idx, arr) =>
s.write(`${e.entry}${idx === arr.length - 1 ? '' : ','}`)
);
s.writeDec('};');
}
}
class CppNamespace implements Outputable {
constructor(name: string) {
this.name = name;
}
name: string;
body: (CppNamespace | CppClass | CppEnum)[] = [];
write(s: BetterWritable) {
s.writeInc(`namespace ${this.name} {`);
this.body.forEach((b) => b.write(s));
s.writeDec('}');
}
}
function _getCppType(
t: TypeNode
): string | ((opt: boolean, name: string) => string) {
switch (t.type) {
case AST_NODE_TYPES.TSStringKeyword:
return 'std::string';
case AST_NODE_TYPES.TSBooleanKeyword:
return 'bool';
case AST_NODE_TYPES.TSNumberKeyword:
return 'long';
case AST_NODE_TYPES.TSTypeReference:
if (t.typeName.type != AST_NODE_TYPES.Identifier) throw new Err(t);
return t.typeName.name;
case AST_NODE_TYPES.TSLiteralType:
if (t.literal.type != AST_NODE_TYPES.Literal) throw new Err(t);
return (opt, name) => `${name} = ${(t.literal as a.Literal).raw}`;
case AST_NODE_TYPES.TSUnionType:
return _getCppType(t.types[0]);
case AST_NODE_TYPES.TSArrayType:
return (opt, name) =>
opt
? `std::optional<${_getCppType(t.elementType)}[]> ${name}`
: `${getCppType(false, name, t.elementType)}[]`;
default:
throw new Err(t);
}
}
function getCppType(opt: boolean, name: string, t: TypeNode): string {
let ret = _getCppType(t);
if (typeof ret === 'string') {
if (opt) ret = `std::optional<${ret}>`;
return `${ret} ${name}`;
} else return `${ret(opt, name)}`;
}
function handleClassProperty(
prop: a.PropertyDefinitionComputedName | a.PropertyDefinitionNonComputedName
): CppProp {
if (prop.key.type != AST_NODE_TYPES.Identifier) throw new Err(prop);
if (!prop.typeAnnotation) throw new Err(prop);
return new CppProp(
getCppType(
prop.optional && prop.optional,
prop.key.name,
prop.typeAnnotation.typeAnnotation
)
);
}
function handleClass(
dec: a.ClassDeclarationWithName | a.ClassDeclarationWithOptionalName
): CppClass {
if (!dec.id) throw new Err(dec);
const cls = new CppClass(dec.id.name);
if (dec.superClass) {
const subCls = dec.superClass;
if (subCls.type == AST_NODE_TYPES.Identifier) cls.sub.push(subCls.name);
else throw new Err(subCls);
}
dec.body.body.forEach((inner) => {
if (inner.type == AST_NODE_TYPES.PropertyDefinition) {
cls.entries.push(handleClassProperty(inner));
} else if (
inner.type == AST_NODE_TYPES.MethodDefinition &&
inner.key.type == AST_NODE_TYPES.Identifier &&
inner.key.name === 'constructor'
)
console.warn('Handle constructors?');
else throw new Err(inner);
});
return cls;
}
function handleEnum(dec: a.TSEnumDeclaration): CppEnum {
const enm = new CppEnum(dec.id.name);
dec.members.forEach((member) => {
if (
member.id.type == AST_NODE_TYPES.Identifier &&
member.initializer.type == AST_NODE_TYPES.Literal
) {
enm.entries.push(
new CppProp(`${member.id.name} = ${member.initializer.raw}`)
);
} else throw new Err(member);
});
return enm;
}
function handleNamedDeclaration(
dec: a.ExportNamedDeclaration
): CppClass | CppEnum | undefined {
if (!dec.declaration) throw new Err(dec);
else if (dec.declaration.type == AST_NODE_TYPES.ClassDeclaration)
return handleClass(dec.declaration);
else if (dec.declaration.type == AST_NODE_TYPES.TSEnumDeclaration)
return handleEnum(dec.declaration);
else if (dec.declaration.type == AST_NODE_TYPES.FunctionDeclaration) return;
else {
throw new Err(dec);
}
}
function handleExport(
ns: CppNamespace,
file: string,
body: a.ExportNamedDeclaration | a.ExportAllDeclaration
) {
let newPath = path.join(path.dirname(file), body.source.value);
if (fs.existsSync(newPath) && fs.statSync(newPath).isDirectory())
newPath = path.join(newPath, 'index.ts');
else if (!newPath.endsWith('.ts')) newPath = newPath + '.ts';
console.log(`${file}: ${newPath}`);
parseFile(ns, newPath);
}
function parseFile(ns: CppNamespace, file: string) {
const ast = parse(fs.readFileSync(file).toString(), {
programs: [program]
});
ast.body
.filter((body) => body.type == AST_NODE_TYPES.ExportNamedDeclaration)
.forEach((body: a.ExportNamedDeclaration) => {
if (!body.source) {
const ret = handleNamedDeclaration(body);
if (ret) ns.body.push(ret);
} else handleExport(ns, file, body);
});
ast.body
.filter((body) => body.type == AST_NODE_TYPES.ExportAllDeclaration)
.forEach((body: a.ExportAllDeclaration) => {
let usedNs = ns;
if ('exported' in body && body.exported) {
const newNs = new CppNamespace(body.exported.name);
ns.body.push(newNs);
usedNs = newNs;
}
handleExport(usedNs, file, body);
});
}
const globalNamespace = new CppNamespace('dto');
parseFile(globalNamespace, 'dto/index.ts');
const output_stream = fs.createWriteStream('dto.h');
output_stream.write('#include <string>\n#include <optional>\n\n');
globalNamespace.write(new BetterWritable(output_stream));