diff --git a/tools/apigen.ts b/tools/apigen.ts index faa1d0d..44f5efa 100644 --- a/tools/apigen.ts +++ b/tools/apigen.ts @@ -4,59 +4,244 @@ import { parse } from '@typescript-eslint/typescript-estree'; import * as fs from 'fs'; -import { - Decorator, - ExportDefaultDeclaration, - FunctionExpression, - Literal -} from '@typescript-eslint/types/dist/generated/ast-spec'; +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'; -const program = createProgram('tsconfig.json'); -const ast = parse(fs.readFileSync('src/controller/filesystem.ts').toString(), { - programs: [program] -}); - -function parseDecorators(decs: Decorator[]): object[] | null { - try { - return decs.map((dec) => { - if (dec.expression.type !== AST_NODE_TYPES.CallExpression) - throw null; - if (dec.expression.callee.type != AST_NODE_TYPES.Identifier) - throw null; - const name = dec.expression.callee.name; - if (name === 'Request') throw null; - const prop = ( - dec.expression.arguments - .filter((arg) => arg.type === AST_NODE_TYPES.Literal) - .pop() as Literal - ).value; - return { name: name, prop: prop }; - }); - } catch (e) { - return null; +class Err extends Error { + constructor(o: object) { + super(); + console.error(o); } } -function parseFunction(name: string, func: FunctionExpression) { - const decs = func.params - .map((p) => p.decorators) - .map(parseDecorators) - .filter((d) => d !== null); - console.log(name, decs); +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); + } } -ast.body - .filter((body) => body.type == AST_NODE_TYPES.ExportDefaultDeclaration) - .forEach((body: ExportDefaultDeclaration) => { - if (body.declaration.type !== AST_NODE_TYPES.ClassDeclaration) return; - if (body.declaration.body.type !== AST_NODE_TYPES.ClassBody) return; - body.declaration.body.body.forEach((def) => { - if ( - def.type === AST_NODE_TYPES.MethodDefinition && - 'name' in def.key && - def.key.name !== 'constructor' && - def.value.type === AST_NODE_TYPES.FunctionExpression - ) - parseFunction(def.key.name, def.value); - }); +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 \n#include \n\n'); +globalNamespace.write(new BetterWritable(output_stream));