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", 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"; 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);