aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/build-declarations.js
diff options
context:
space:
mode:
authorsigoden <sigoden@gmail.com>2024-06-07 15:16:31 +0800
committerGitHub <noreply@github.com>2024-06-07 15:16:31 +0800
commit739a832d87c00e3b5977a24bba5654fa5ea7a702 (patch)
tree2bb4c102a3e04b9c8c1ecd61bdb6c92f84ca27cb /scripts/build-declarations.js
parent2b07fc2c7e4e6311d35ae72c17b25e47680d61f6 (diff)
downloadllm-functions-docker-739a832d87c00e3b5977a24bba5654fa5ea7a702.tar.gz
feat: js/py generate declarations from comments (#30)
Diffstat (limited to 'scripts/build-declarations.js')
-rw-r--r--scripts/build-declarations.js203
1 files changed, 203 insertions, 0 deletions
diff --git a/scripts/build-declarations.js b/scripts/build-declarations.js
new file mode 100644
index 0000000..e1a2892
--- /dev/null
+++ b/scripts/build-declarations.js
@@ -0,0 +1,203 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+
+function main() {
+ const scriptfile = process.argv[2];
+ const contents = fs.readFileSync(process.argv[2], "utf8");
+ const functions = extractFunctions(contents);
+ let declarations = functions.map(({ funcName, jsdoc }) => {
+ const { description, params } = parseJsDoc(jsdoc, funcName);
+ const declaration = buildDeclaration(funcName, description, params);
+ return declaration;
+ });
+ const name = getBasename(scriptfile);
+ if (declarations.length > 0) {
+ declarations = declarations.slice(0, 1);
+ declarations[0].name = name;
+ }
+ console.log(JSON.stringify(declarations, null, 2));
+}
+
+/**
+ * @param {string} contents
+ * @param {bool} isTool
+ */
+function extractFunctions(contents, isTool = true) {
+ const output = [];
+ const lines = contents.split("\n");
+ let isInComment = false;
+ let jsdoc = "";
+ let incompleteComment = "";
+ for (let line of lines) {
+ if (/^\s*\/\*/.test(line)) {
+ isInComment = true;
+ incompleteComment += `\n${line}`;
+ } else if (/^\s*\*\//.test(line)) {
+ isInComment = false;
+ incompleteComment += `\n${line}`;
+ jsdoc = incompleteComment;
+ incompleteComment = "";
+ } else if (isInComment) {
+ incompleteComment += `\n${line}`;
+ } else {
+ if (!jsdoc || line.trim() === "") {
+ continue;
+ }
+ if (isTool) {
+ if (/function main/.test(line)) {
+ output.push({
+ funcName: "main",
+ jsdoc,
+ });
+ }
+ } else {
+ const match = /function *([_A-Za-z]+)/.exec(line);
+ if (match) {
+ const funcName = match[1];
+ if (!funcName.startsWith("_")) {
+ output.push({ funcName, jsdoc });
+ }
+ }
+ }
+ jsdoc = "";
+ }
+ }
+ return output;
+}
+
+/**
+ * @param {string} jsdoc
+ * @param {string} funcName,
+ */
+function parseJsDoc(jsdoc, funcName) {
+ const lines = jsdoc.split("\n");
+ let description = "";
+ const rawParams = [];
+ let tag = "";
+ for (let line of lines) {
+ line = line.replace(/^\s*(\/\*\*|\*\/|\*)/, "").trim();
+ let match = /^@(\w+)/.exec(line);
+ if (match) {
+ tag = match[1];
+ }
+ if (!tag) {
+ description += `\n${line}`;
+ } else if (tag == "property") {
+ if (match) {
+ rawParams.push(line.slice(tag.length + 1).trim());
+ } else {
+ rawParams[rawParams.length - 1] += `\n${line}`;
+ }
+ }
+ }
+ const params = [];
+ for (const rawParam of rawParams) {
+ try {
+ params.push(parseParam(rawParam));
+ } catch (err) {
+ throw new Error(
+ `Unable to parse function '${funcName}' of jsdoc '@property ${rawParam}'`,
+ );
+ }
+ }
+ return {
+ description: description.trim(),
+ params,
+ };
+}
+
+/**
+ * @typedef {ReturnType<parseParam>} Param
+ */
+
+/**
+ * @param {string} rawParam
+ */
+function parseParam(rawParam) {
+ const regex = /^{([^}]+)} +(\S+)( *- +| +)?/;
+ const match = regex.exec(rawParam);
+ if (!match) {
+ throw new Error(`Invalid jsdoc comment`);
+ }
+ const type = match[1];
+ let name = match[2];
+ const description = rawParam.replace(regex, "");
+
+ let required = true;
+ if (/^\[.*\]$/.test(name)) {
+ name = name.slice(1, -1);
+ required = false;
+ }
+ let property = buildProperty(type, description);
+ return { name, property, required };
+}
+
+/**
+ * @param {string} type
+ * @param {string} description
+ */
+function buildProperty(type, description) {
+ type = type.toLowerCase();
+ const property = {};
+ if (type.includes("|")) {
+ property.type = "string";
+ property.enum = type.replace(/'/g, "").split("|");
+ } else if (type === "boolean") {
+ property.type = "boolean";
+ } else if (type === "string") {
+ property.type = "string";
+ } else if (type === "integer") {
+ property.type = "integer";
+ } else if (type === "number") {
+ property.type = "number";
+ } else if (type === "string[]") {
+ property.type = "array";
+ property.items = { type: "string" };
+ } else {
+ throw new Error(`Unsupported type '${type}'`);
+ }
+ property.description = description;
+ return property;
+}
+
+/**
+ * @param {string} filePath
+ */
+function getBasename(filePath) {
+ const filenameWithExt = filePath.split(/[/\\]/).pop();
+
+ const lastDotIndex = filenameWithExt.lastIndexOf(".");
+
+ if (lastDotIndex === -1) {
+ return filenameWithExt;
+ }
+
+ return filenameWithExt.substring(0, lastDotIndex);
+}
+
+/**
+ * @param {string} name
+ * @param {string} description
+ * @param {Param[]} params
+ */
+function buildDeclaration(name, description, params) {
+ const schema = {
+ name,
+ description,
+ properties: {},
+ };
+ const requiredParams = [];
+ for (const { name, property, required } of params) {
+ schema.properties[name] = property;
+ if (required) {
+ requiredParams.push(name);
+ }
+ }
+ if (requiredParams.length > 0) {
+ schema.required = requiredParams;
+ }
+ return schema;
+}
+
+main();