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); } writeDecInc(data: string) { this.indent--; this.write(data); this.indent++; } } interface Outputable { write(s: BetterWritable); writeJsonAs(ns: string, s: BetterWritable); } class CppProp { constructor(entry: string) { this.entry = entry; } entry: string; } // template <> inline dto::Requests::Auth::LoginRequest Json::Value::as() const { return asFloat(); } class CppClass implements Outputable { constructor(name: string) { this.name = name; } type = 'class' as const; name: string; sub: string[] = []; entries: CppProp[] = []; write(s: BetterWritable) { let name = `struct ${this.name}`; if (this.sub.length > 0) name += ' : public ' + this.sub.join(', public '); s.writeInc(`${name} {`); if (this.sub.length == 0) { s.write(`${this.name}() = default;`); s.writeInc(`explicit ${this.name}(const Json::Value& j) {`); } else { s.writeInc(`${this.name}() :`); this.sub.forEach((sub) => { s.write(`${sub}()`); }); s.writeDec('{}'); s.writeInc(`explicit ${this.name}(const Json::Value& j) :`); this.sub.forEach((sub) => { s.write(`${sub}(j)`); }); s.writeDecInc('{'); } this.entries.forEach((e) => { const [type, name] = e.entry.split(' '); const opt_match = /std::optional<([a-z:<>]+)>/.exec(type); if (opt_match !== null) s.write( `this->${name} = json_get_optional<${opt_match[1]}>(j, "${name}");` ); else s.write(`this->${name} = j["${name}"].as<${type}>();`); }); s.writeDec('}'); this.entries.forEach((e) => s.write(`${e.entry};`)); s.writeDec('};'); } writeJsonAs(ns: string, s: BetterWritable) { s.write( `template <> inline ${ns}${this.name} Json::Value::as() const { return ${ns}${this.name}(*this); }` ); } } class CppEnum implements Outputable { constructor(name: string) { this.name = name; } type = 'enum' as const; name: string; entries: CppProp[] = []; write(s: BetterWritable) { return; s.writeInc(`enum ${this.name} : int {`); this.entries.forEach((e, idx, arr) => s.write(`${e.entry}${idx === arr.length - 1 ? '' : ','}`) ); s.writeDec('};'); } writeJsonAs(ns: string, s: BetterWritable) { return; s.write( `template <> inline ${ns}${this.name} Json::Value::as() const { return (${ns}${this.name})(asInt()); }` ); } } class CppNamespace implements Outputable { constructor(name: string) { this.name = name; } type = 'ns' as const; name: string; body: (CppNamespace | CppClass | CppEnum)[] = []; write(s: BetterWritable) { s.writeInc(`namespace ${this.name} {`); this.body.forEach((b) => b.write(s)); s.writeDec('}'); } writeJsonAs(ns: string, s: BetterWritable) { ns += `${this.name}::`; this.body.forEach((b) => b.writeJsonAs(ns, s)); } } function _getCppType( t: TypeNode ): null | 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 'int'; 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: return null; case AST_NODE_TYPES.TSUnionType: return _getCppType(t.types[0]); case AST_NODE_TYPES.TSArrayType: return `std::vector<${_getCppType(t.elementType)}>`; default: throw new Err(t); } } function getCppType(opt: boolean, name: string, t: TypeNode): null | string { let ret = _getCppType(t); if (typeof ret === 'function') return `${ret(opt, name)}`; if (typeof ret === 'string') { if (opt) ret = `std::optional<${ret}>`; return `${ret} ${name}`; } else return ret; } function handleClassProperty( prop: a.PropertyDefinitionComputedName | a.PropertyDefinitionNonComputedName ): CppProp | null { if (prop.key.type != AST_NODE_TYPES.Identifier) throw new Err(prop); if (!prop.typeAnnotation) throw new Err(prop); const type = getCppType( prop.optional && prop.optional, prop.key.name, prop.typeAnnotation.typeAnnotation ); return type === null ? null : new CppProp(type); } 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) { const prop = handleClassProperty(inner); if (prop) cls.entries.push(prop); } else if ( inner.type == AST_NODE_TYPES.MethodDefinition && inner.key.type == AST_NODE_TYPES.Identifier && inner.key.name === 'constructor' ) return; 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('../backend/src/dto.h'); output_stream.write( '#pragma once\n\n' + '#include \n' + '#include \n' + '#include \n' + '#include \n' + '#include "dto_extras.h"\n\n' ); const output = new BetterWritable(output_stream); globalNamespace.write(output); globalNamespace.writeJsonAs('', output);