@iovalkey/interface-generator
Version:
Generate interface for iovalkey
415 lines (391 loc) • 11.3 kB
JavaScript
const camelcase = require("lodash.camelcase");
const flatMap = require("lodash.flatmap");
const Valkey = require("iovalkey");
const { pluralize, singularize } = require("inflection");
const { packObject, addMatrix, getCommandParameters } = require("./utils");
const processArguments = (
{ name: rawName, type, token, arguments: rawArgs, flags = [] },
rawAdd,
typeMaps
) => {
const add = (defs) => {
rawAdd(defs.filter(Boolean).map((items) => items.filter(Boolean)));
};
const args = rawArgs && rawArgs.map(packObject);
let name = camelcase(rawName.toLowerCase());
const tokenDef = token
? {
name: camelcase(token.toLowerCase()) || name,
type: `'${token.toUpperCase()}'`,
}
: null;
const optional = flags.includes("optional");
const multiple = flags.includes("multiple");
switch (type) {
case "key":
add([
optional && [],
[
tokenDef && { ...tokenDef, name: `${name}Token` },
{ name, type: typeMaps.key, multiple },
],
]);
break;
case "string":
if (name === "function") {
name = "fun";
}
add([
optional && [],
[
tokenDef && { ...tokenDef, name: `${name}Token` },
{
name,
type:
typeof typeMaps.string === "function"
? typeMaps.string(name)
: typeMaps.string,
multiple,
},
],
]);
break;
case "pattern":
add([
optional && [],
[
tokenDef && { ...tokenDef, name: `${name}Token` },
{ name, type: typeMaps.pattern, multiple },
],
]);
break;
case "integer":
case "double":
case "unix-time":
add([
optional && [],
[
tokenDef && { ...tokenDef, name: `${name}Token` },
{
name,
type:
typeof typeMaps.number === "function"
? typeMaps.number(name)
: typeMaps.number,
multiple,
},
],
]);
break;
case "pure-token":
add([optional && [], [tokenDef]]);
break;
case "oneof": {
const overrides = [];
args.forEach((arg) =>
processArguments(
arg,
(processed) => {
overrides.push(...processed);
},
typeMaps
)
);
if (tokenDef) {
overrides.forEach((override) => {
override.unshift(tokenDef);
});
}
add([optional && [], ...overrides]);
break;
}
case "block": {
let overrides = [[]];
args.forEach((arg) =>
processArguments(
arg,
(processed) => {
overrides = addMatrix(overrides, processed);
},
typeMaps
)
);
if (multiple) {
if (overrides.length !== 1) {
throw new Error("Overrides is not supported for blocks");
}
overrides.forEach((override) => {
override.forEach(({ multiple }) => {
if (multiple !== false) {
throw new Error("Multiple is not supported for blocks");
}
});
});
add([
optional && [],
[
tokenDef && { ...tokenDef, name: `${name}Token` },
{
name,
type: `[${overrides[0]
.map(({ name, type }) => `${name}: ${type}`)
.join(", ")}]`,
multiple: true,
},
],
]);
} else {
if (tokenDef) {
overrides.forEach((override) => {
override.unshift({ ...tokenDef, name: `${name}Token` });
});
}
if (optional) {
overrides.unshift([]);
}
add(overrides);
}
break;
}
default:
throw new Error(`Unsupported type ${rawName}: ${type}`);
}
};
const processCommand = (
def,
subcommandArgs,
allDefs,
argumentTypes,
typeMaps,
sortArguments
) => {
let argsDefs = [[]];
if (argumentTypes[def.name]) {
argsDefs = argumentTypes[def.name];
} else if (def.arguments) {
const args = def.arguments.map(packObject);
(sortArguments[def.name] ? sortArguments[def.name](args) : args).forEach(
(argument) => {
processArguments(
argument,
(argDefs) => {
argsDefs = addMatrix(argsDefs, argDefs);
},
typeMaps
);
}
);
}
allDefs.push({
name: def.name,
summary: def.summary,
group: def.group,
complexity: def.complexity,
since: def.since,
argDefs: argsDefs.map((defs) => subcommandArgs.concat(defs)),
});
};
const shouldProvideArrayVariant = ({ name, type }, typeMaps) => {
return (
type === typeMaps.key || ["slot", "member"].includes(singularize(name))
);
};
const typeOrder = ["string", "Buffer", "number"];
const uniqTypes = (types) =>
[...new Set(types.split(" | "))]
.sort((a, b) => typeOrder.indexOf(a) - typeOrder.indexOf(b))
.join(" | ");
const convertToMultipleTypes = ({ name, type }, keysToArray, typeMaps) => {
if (keysToArray && shouldProvideArrayVariant({ name, type }, typeMaps)) {
return `${pluralize(name)}: (${type})[]`;
}
const multipleType = type.includes("[")
? `(${uniqTypes(
type
.replace(/^\[/, "")
.replace(/\]$/, "")
.split(",")
.map((a) => a.split(":").pop().trim())
.join(" | ")
)})[]`
: `(${type})[]`;
return `...${pluralize(name)}: ${multipleType}`;
};
function processSubcommands(
def,
allDefs,
argumentTypes,
typeMaps,
sortArguments
) {
def.subcommands = packObject(def.subcommands);
Object.keys(def.subcommands)
.sort()
.forEach((subcommand) => {
const subDef = Object.assign(packObject(def.subcommands[subcommand]), {
name: def.name,
});
const commands = subcommand.split("|").slice(1);
processCommand(
subDef,
commands.map((name) => ({
name:
commands.length > 1 ? camelcase(name.toLowerCase()) : "subcommand",
type: `'${name.toUpperCase()}'`,
})),
allDefs,
argumentTypes,
typeMaps,
sortArguments
);
});
}
async function getCommanderInterface({
commands,
complexityLimit,
valkeyOpts,
overrides,
returnTypes,
argumentTypes,
sortArguments,
typeMaps,
ignoredBufferVariant = [],
}) {
const allDefs = [];
const valkey = new Valkey(valkeyOpts);
for (const command of commands) {
try {
const result = packObject((await valkey.command("docs", command))[1]);
Object.assign(result, { name: command });
if (result.subcommands) {
processSubcommands(
result,
allDefs,
argumentTypes,
typeMaps,
sortArguments
);
} else {
processCommand(
result,
[],
allDefs,
argumentTypes,
typeMaps,
sortArguments
);
}
} catch (err) {
console.error(`Failed to parse command: ${command} with err:`, err);
}
}
const generatedMethodDeclarations = allDefs
.map(({ name, summary, group, complexity, since, argDefs: rawArgDefs }) => {
let argDefs = rawArgDefs;
if (rawArgDefs.length > complexityLimit) {
argDefs = [getCommandParameters(rawArgDefs)];
}
const description = `
/**
* ${summary}
* - _group_: ${group}
* - _complexity_: ${complexity}
* - _since_: ${since}
*/`;
let generatedFunctionDeclarations = flatMap(argDefs, (def) => {
let returnType = "unknown";
if (typeof returnTypes[name] === "function") {
returnType =
returnTypes[name](def.map(({ type }) => type)) || "unknown";
} else if (returnTypes[name]) {
returnType = returnTypes[name];
}
const hasMultipleParameter = def.find(({ multiple }) => multiple);
return flatMap(hasMultipleParameter ? [1, 0] : [2], (withCallback) => {
return flatMap(
def.find(
({ name, type, multiple }) =>
multiple && shouldProvideArrayVariant({ name, type }, typeMaps)
)
? [false, true]
: [false],
(keysToArray) => {
return (
(name === "exec" || returnType.includes("string")) &&
!ignoredBufferVariant.includes(name)
? [false, true]
: [false]
).map((withBuffer) => {
let localDef = def.slice(0);
const localReturnType = withBuffer
? returnType.replace(/string/g, "Buffer")
: returnType;
if (withCallback) {
localDef.push({
name: "callback",
optional: withCallback === 2,
type: `Callback<${localReturnType}>`,
});
}
const argNameUsedTimes = {};
let parameters = "";
localDef = localDef.map((item) => {
const argName = item.name;
const usedTimes = argNameUsedTimes[argName] || 0;
argNameUsedTimes[argName] = usedTimes + 1;
const uniqueName = usedTimes
? `${argName}${usedTimes}`
: argName;
return {
...item,
name: uniqueName,
};
});
if (hasMultipleParameter) {
parameters = `...args: [${localDef
.map((item) => {
return item.multiple
? convertToMultipleTypes(item, keysToArray, typeMaps)
: `${item.name}: ${item.type}`;
})
.join(", ")}]`;
} else {
parameters = localDef
.map((item) => {
return item.multiple
? convertToMultipleTypes(item, keysToArray, typeMaps)
: `${item.name}${item.optional ? "?" : ""}: ${
item.type
}`;
})
.join(", ");
}
const methodName = withBuffer ? `${name}Buffer` : name;
let result = `
${
methodName.includes("-") ? `['${methodName}']` : methodName
}(${parameters}): Result<${localReturnType}, Context>;`;
return result;
});
}
);
});
});
return (
description +
(overrides[name]
? "\n" +
overrides[name].defs
.map((line) => " " + line.replace("$1", name))
.join("\n")
: "") +
(overrides[name] && overrides[name].overwrite
? ""
: generatedFunctionDeclarations.join(""))
);
})
.join("\n");
return generatedMethodDeclarations;
}
module.exports = getCommanderInterface;