link-stack/packages/node-signald/util/generate.js

230 lines
5.6 KiB
JavaScript
Raw Normal View History

2023-03-13 10:47:38 +00:00
const net = require("net");
const fs = require("fs");
const process = require("process");
const camelCase = require("camelcase");
const SOCKETFILE =
process.env.SIGNALD_SOCKET || "/run/user/1000/signald/signald.sock";
const GENERATEDFILE = "./src/generated.ts";
const VERSION = "v1";
let globalVersion;
const toJs = (name) => name.replace("-", "_");
const TYPES = {
String: "string",
long: "number",
int: "number",
Integer: "number",
Map: "Record<string, string>",
boolean: "boolean",
Boolean: "boolean",
Long: "number",
UUID: "string",
};
const convertType = (field) => {
let name = "";
if (field.version) {
name = `${field.type}${field.version}`;
} else {
if (!TYPES[field.type]) {
throw new Error(
`unknown type ${field.type} inside ${JSON.stringify(field)}`
);
}
name = TYPES[field.type];
}
if (field.list) {
return `${name}[]`;
}
return name;
};
const fieldToData = ([rawName, field]) => {
const name = toJs(rawName);
const type = convertType(field);
const requiredStr = field.required ? "" : "?";
const example = field.example ? `\n Example: ${field.example}\n` : "\n";
const doc = field.doc ? `\n/**\n ${field.doc}.${example} */\n` : "";
let code = `${doc}${name}${requiredStr}: ${type};`;
return {
name,
rawName,
type,
code,
doc: field.doc,
example: field.example,
required: field.required,
};
};
const typeToData = (name, version, data) => {
const typeName = `${name}${version}`;
const doc = data.doc
? `\n/**
${data.doc}
*/\n`
: "\n";
const header = `export type ${typeName} = {
`;
const dataFields = Object.entries(data.fields).map(fieldToData);
const codeFields = dataFields.map((field) => field.code);
const body = codeFields.join("\n");
const footer = "\n}\n";
// ensure required fields are sorted before non-required fields
dataFields.sort((a, b) =>
a.required === b.required ? 0 : a.required ? -1 : 1
);
return {
name,
version,
typeName,
fields: dataFields,
code: doc + header + body + footer,
};
};
const actionToCode = (structures, rawName, version, data) => {
const name = camelCase(rawName);
const { request, response } = data;
const requestFields = structures[request].fields;
const returnTypeStr = response
? `Promise<${response}${version}>`
: "Promise<void>";
let code = ` async ${name}(`;
requestFields.map((field) => {
const requiredStr = field.required ? "" : "?";
code += `${field.name}${requiredStr}: ${field.type},\n`;
});
code += `): ${returnTypeStr} {\n`;
const params = requestFields
.map((field) => {
return `"${field.rawName}": ${field.name}`;
})
.join(",\n");
const requestStr = `{"type": "${rawName}","version": "${version}", ${params}}`;
const responseStr = `this.getResponse(${requestStr})`;
if (response) {
code += `return ${responseStr} as Promise<${response}${version}>`;
} else {
code += `await ${responseStr}; return;`;
}
code += "\n}\n";
return code;
};
const saveFile = (code) => {
fs.writeFileSync(GENERATEDFILE, code);
console.log("generated code at ", GENERATEDFILE);
};
const processProtocolDefinition = (client, def, version) => {
console.log("processing protocol definition");
const when = new Date(new Date().toUTCString());
let contents = `
/* Generated by the output of the 'protocol' command of signald
Date: ${when}
Version: ${globalVersion}
*/
import {JSONTransport} from "./util";
`;
const structures = {};
// generate type definitions
for (version in def.types) {
Object.entries(def.types[version]).map(([name, data]) => {
structures[name] = typeToData(name, version, data);
contents += structures[name].code;
});
}
// TODO this should be improved once v0 is totally deprecated
const typeNames = ["version", "message", "subscribe", "unsubscribe"]
.map((name) => `"${name}"`)
.join(" | ");
contents += `\ndeclare module "./util" {
interface EventTypes {
/**
* The v0 event emitted when a signal message is received
* @event
*/
messagev0(message: JsonMessageEnvelopev1): void;
/**
* @event
*/
subscribev0(): void;
/**
* @event
*/
unsubscribev0(): void;
}
}\n`;
// generate action functions
for (version in def.actions) {
if (version != VERSION) continue;
contents += "export class SignaldGeneratedApi extends JSONTransport{\n";
Object.entries(def.actions[version]).map(([name, data]) => {
contents += actionToCode(structures, name, version, data);
});
contents += "\n}\n";
}
saveFile(contents);
cleanup();
};
// := Driver
const getProtocolDefinition = (client) => {
const msg = {
type: "protocol",
};
console.log("requesting protocol definition");
client.write(JSON.stringify(msg) + "\n");
};
const client = net.createConnection(SOCKETFILE);
let buffer = "";
client.setEncoding("utf8");
function cleanup() {
client.end();
process.exit(0);
}
client.on("connect", () => {
console.log("connected");
buffer = "";
});
client.on("data", (payload) => {
buffer += payload;
if (!buffer.endsWith("\n")) {
return;
} else {
const data = buffer;
buffer = "";
data.split("\n").forEach((line) => {
if (!line) return;
const msg = JSON.parse(line);
if (msg.type === "version") {
getProtocolDefinition(client);
globalVersion = msg.data.version;
}
if (msg.type === "protocol") {
// console.log(JSON.stringify(msg.data, null, 2));
processProtocolDefinition(client, msg.data, globalVersion);
}
});
}
});
client.on("error", (err) => {
console.error(err);
});
process.on("SIGINT", cleanup);