307 lines
8.0 KiB
TypeScript
307 lines
8.0 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);
|
|
}
|
|
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<dto::Requests::Auth::LoginRequest>() 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 <string>\n' +
|
|
'#include <optional>\n' +
|
|
'#include <vector>\n' +
|
|
'#include <json/allocator.h>\n' +
|
|
'#include "dto_extras.h"\n\n'
|
|
);
|
|
const output = new BetterWritable(output_stream);
|
|
globalNamespace.write(output);
|
|
globalNamespace.writeJsonAs('', output);
|