229 lines
5.6 KiB
JavaScript
229 lines
5.6 KiB
JavaScript
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);
|