Skip to content

Commit

Permalink
fix(commands): support arrays in the middle of command arguments (#26)
Browse files Browse the repository at this point in the history
fixes #20
  • Loading branch information
mmkal committed Sep 23, 2018
1 parent e593c7c commit e116779
Show file tree
Hide file tree
Showing 12 changed files with 1,583 additions and 422 deletions.
7 changes: 7 additions & 0 deletions .env.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# any variables set in here, you can override by creating a `.env` file.

# by default logging is disabled - logs only happen when they match a regex
# and the one here impossible to match: https://stackoverflow.com/a/2302992)
LOG_REGEX=^\b$
WARN_REGEX=^\b$
ERROR_LOG_REGEX=^\b$
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ todo.md

# local package testing
handy-redis-0.0.0-development.tgz

# local environment variable overrides
.env
71 changes: 71 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"coveralls": "^3.0.2",
"cross-spawn": "^6.0.5",
"del-cli": "^1.1.0",
"dotenv-extended": "^2.3.0",
"lodash": "^4.17.10",
"npm-run-all": "^4.1.3",
"nyc": "^13.0.1",
Expand Down
11 changes: 6 additions & 5 deletions scripts/cli-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@ import { commandDoc, readdirWithPaths } from "./util";
import { readFileSync } from "fs";
import * as _ from "lodash";
import * as spawn from "cross-spawn";
import { log, warn, error } from "./log";

export const getExampleRuns = async () => {
const examples = getCliExamples();
const redisCli = spawn("docker", ["exec", "-i", "handy_redis", "redis-cli", "--no-raw"], { env: process.env });
redisCli.stdin.setDefaultEncoding("utf-8");
const redisInteractor = {
onstdout: (data: string) => console.log(data),
onstderr: (data: string) => console.warn(data),
onstdout: (data: string) => log(data),
onstderr: (data: string) => warn(data),
sendCommand: (command: string) => new Promise<Output>((resolve, reject) => {
redisInteractor.onstdout = data => {
console.log(data);
log(data);
resolve(parseCommandOutput(command, data, null));
};
redisInteractor.onstderr = data => {
console.error(data);
error(data);
resolve(parseCommandOutput(command, null, data));
};
console.log(">", command);
log(">", command);
redisCli.stdin.write(`${command}\n`);
}),
};
Expand Down
58 changes: 17 additions & 41 deletions scripts/command/generate.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { stringify as yamlify } from "yamljs";
import { Command, BasicCommandInfo, Argument, TypeScriptArg, CommandCollection } from "./types";
import { getOverloads } from "../overloads";
import { EOL } from "os";
import { readFileSync } from "fs";
import { redisDoc, simplifyName, makeArrayType, getDocs } from "../util";
import * as _ from "lodash";
import { createClient } from "redis";
import { flattenDeep } from "../../src/flatten";

const warn = (...args: any[]) => {
// console.warn.apply(null, args);
};
const error: typeof console.error = (...args: any[]) => {
// console.error.apply(null, args);
};
import { warn } from "../log";

const typeFor = (arg: Argument): string => {
if (arg.command) {
Expand Down Expand Up @@ -41,7 +33,7 @@ const typeFor = (arg: Argument): string => {
if (literalValueRegex.test(arg.type)) {
return arg.type;
}
console.warn(`Argument ${JSON.stringify(arg)} has unknown type "${arg.type}"`);
warn(`Argument ${JSON.stringify(arg)} has unknown type "${arg.type}"`);
return "any";
}
}
Expand All @@ -53,7 +45,10 @@ const buildTypeScriptCommandInfo = (name: string, command: Command): BasicComman
if (a.name) {
nameParts.push(a.name);
}
if (a.command) {
const previousArgsWithSameName = all
.slice(0, index)
.filter(other => other !== a && other.name === a.name);
if (a.command && (nameParts.length === 0 || previousArgsWithSameName.length > 0)) {
nameParts.unshift(a.command);
}
const argJson = JSON.stringify(a);
Expand All @@ -72,38 +67,19 @@ const buildTypeScriptCommandInfo = (name: string, command: Command): BasicComman
});

return getOverloads(baseArgs).map(allArgs => {
const numMultiples = allArgs.filter(a => a.multiple).length;
let firstMultipleIndex = allArgs.findIndex(a => !!a.multiple);
if (firstMultipleIndex === -1) {
firstMultipleIndex = allArgs.length;
}
if (firstMultipleIndex >= 0 && firstMultipleIndex !== allArgs.length - numMultiples) {
error(`"multiple" argument appears before the end of argument list:${EOL}${yamlify(allArgs)}.`);
return null;
}

const typescriptArgs: TypeScriptArg[] = allArgs
.slice(0, firstMultipleIndex)
.map(a => ({ name: a.name, type: typeFor(a) }))
;

if (numMultiples === 1) {
const lastArg = allArgs[allArgs.length - 1];
const argName = lastArg.name.replace("argument", "arg");
typescriptArgs.push({
name: `...${argName}s`,
type: makeArrayType(typeFor(lastArg)),
});
} else if (numMultiples > 1) {
const multipleArgs = allArgs.slice(firstMultipleIndex);
const tupleName = numMultiples === 2 ? "pair" : "tuple";
const grouping = multipleArgs.map(a => a.name).concat([tupleName]).join("_");
const groupingsTypes = multipleArgs.map(typeFor).join(", ");
typescriptArgs.push({
name: `...${grouping}s`,
type: makeArrayType(`[${groupingsTypes}]`),
.map((argument, index, list) => {
let tsArgName = argument.name.replace("argument", "arg");
let tsArgType = typeFor(argument);
if (argument.multiple) {
tsArgName += "s";
tsArgType = makeArrayType(tsArgType);
if (index === list.length - 1 && allArgs.filter(a => a.multiple).length <= 1) {
tsArgName = "..." + tsArgName;
}
}
return { name: tsArgName, type: tsArgType };
});
}

const docs = getDocs(command);

Expand Down
14 changes: 6 additions & 8 deletions scripts/generate-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { quote, simplifyName, tab, indent, buildScript, writeFile } from "./util
import * as _ from "lodash";
import { getExampleRuns } from "./cli-examples";
import { getBasicCommands } from "./command";
import { warn } from "./log";

const tokenizeCommand = (command: string) => {
return (command
Expand Down Expand Up @@ -109,13 +110,12 @@ const formatLiteralArgumentFromOverload = (overloadInfo: BasicCommandInfo, liter
const { type } = arg;
const arrayMatch = type.match(arrayRegex);
const isTuple = tupleRegex.test(type);
const isVariadic = arg.name.startsWith("...");

/** Formats the next literal token into the target list, coercing it into the target type first */
const nextFormattedToken = (targetType = type) => {
const literal = literalTokens[nextLiteralIndex++];
if (typeof literal === "undefined") {
console.warn(`Ran out of literal tokens. command ${overloadInfo.name}, tokens: ${literalTokens.join(" ")}`);
warn(`Ran out of literal tokens. command ${overloadInfo.name}, tokens: ${literalTokens.join(" ")}`);
}
return targetType === "number" ? parseNumber(literal).toString() : quote(literal);
};
Expand All @@ -134,19 +134,17 @@ const formatLiteralArgumentFromOverload = (overloadInfo: BasicCommandInfo, liter
return `[${formattedTupleParts.join(", ")}]`;
};

if (arrayMatch && isVariadic) {
if (isTuple) { // todo use ternary like above
const nextArg = nextFormattedTuple(type);
formattedArgs.push(nextArg);
} else if (arrayMatch) {
const itemType = arrayMatch[1] || arrayMatch[2];
const getNext = tupleRegex.test(itemType)
? nextFormattedTuple
: nextFormattedToken;
while (nextLiteralIndex < literalTokens.length) {
formattedArgs.push(getNext(itemType));
}
} else if (arrayMatch) {
throw new Error(`This is a genuine error. This library doesn't support arbitrary arrays not at the end of the argument list`);
} else if (isTuple) { // todo use ternary like above
const nextArg = nextFormattedTuple(type);
formattedArgs.push(nextArg);
} else {
// regular arg
const nextArg = nextFormattedToken();
Expand Down
21 changes: 10 additions & 11 deletions scripts/generate-usages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getCliExamples } from "./cli-examples";
import { getBasicCommands, FullCommandInfo } from "./command";
import * as tsc from "typescript";
import { readFileSync } from "fs";
import { warn } from "./log";

const tokenizeCommand = (command: string) => {
return (command
Expand Down Expand Up @@ -110,13 +111,12 @@ const formatLiteralArgumentFromOverload = (overloadInfo: BasicCommandInfo, liter
const { type } = arg;
const arrayMatch = type.match(arrayRegex);
const isTuple = tupleRegex.test(type);
const isVariadic = arg.name.startsWith("...");

/** Formats the next literal token into the target list, coercing it into the target type first */
const nextFormattedToken = (targetType = type) => {
const literal = literalTokens[nextLiteralIndex++];
if (typeof literal === "undefined") {
console.warn(`Ran out of literal tokens. command ${overloadInfo.name}, tokens: ${literalTokens.join(" ")}`);
warn(`Ran out of literal tokens. command ${overloadInfo.name}, tokens: ${literalTokens.join(" ")}`);
}
return targetType === "number" ? parseNumber(literal).toString() : quote(literal);
};
Expand All @@ -135,19 +135,17 @@ const formatLiteralArgumentFromOverload = (overloadInfo: BasicCommandInfo, liter
return `[${formattedTupleParts.join(", ")}]`;
};

if (arrayMatch && isVariadic) {
if (isTuple) { // todo use ternary like above
const nextArg = nextFormattedTuple(type);
formattedArgs.push(nextArg);
} else if (arrayMatch) {
const itemType = arrayMatch[1] || arrayMatch[2];
const getNext = tupleRegex.test(itemType)
? nextFormattedTuple
: nextFormattedToken;
while (nextLiteralIndex < literalTokens.length) {
formattedArgs.push(getNext(itemType));
}
} else if (arrayMatch) {
throw new Error(`This is a genuine error. This library doesn't support arbitrary arrays not at the end of the argument list`);
} else if (isTuple) { // todo use ternary like above
const nextArg = nextFormattedTuple(type);
formattedArgs.push(nextArg);
} else {
// regular arg
const nextArg = nextFormattedToken();
Expand Down Expand Up @@ -209,19 +207,20 @@ export const getReturnValuesFuncSrc = async () => {

return [
`try {`,
` console.log("running", "${command}", ${args});`,
` logger.log("running", "${command}", ${args});`,
` const value = await client.${command}(${args});`,
` getOrCreate(${quote(formatted.overload.name)}).push(value);`,
` console.log("ran", "${command}", ${args});`,
` logger.log("ran", "${command}", ${args});`,
`} catch (e) {`,
` console.error(e);`,
` logger.error(e);`,
`}`,
].join(EOL);
}));

const getReturnValuesTs = [
`async (client) => {`,
` await client.ping();`,
` const logger = require("${__dirname.replace(/\\/g, "/")}/log")`,
` const returnValues = new Map<string, any[]>();`,
` const getOrCreate = (commandName: string) => {`,
` if (!returnValues.has(commandName)) {`,
Expand Down
13 changes: 13 additions & 0 deletions scripts/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { load } from "dotenv-extended";

load();

const maybeLog = (logFn: Function, regex: string | undefined) => (...args: any[]) => {
if (process.env.VERBOSE || regex === "" || args.join(" ").match(regex || ".*")) {
logFn(...args);
}
};
// tslint:disable no-console
export const warn = maybeLog(console.warn, process.env.WARN_REGEX);
export const log = maybeLog(console.log, process.env.LOG_REGEX);
export const error = maybeLog(console.error, process.env.ERROR_LOG_REGEX);
2 changes: 1 addition & 1 deletion scripts/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const run = async (task: () => void | Promise<any>) => {
await task();
process.exit(0);
} catch (e) {
console.error(e);
console.error(e); // tslint:disable-line no-console
process.exit(1);
}
};
Expand Down

0 comments on commit e116779

Please sign in to comment.