Skip to content

Commit

Permalink
Named tuple types (#254)
Browse files Browse the repository at this point in the history
* Use named tuple types

This applies a few improvements to how things are named, and uses the new TypeScript named tuple type feature to make complex commands which require tuple inputs more readable.

Example:
Before:
```
xgroup(delconsumer_key_groupname_consumername?: ["DELCONSUMER", [string, string, string]]): Promise<unknown>;
```

After:
```
xgroup(
        delconsumer?: [
            delconsumer: "DELCONSUMER",
            key_groupname_consumername: [key: string, groupname: string, consumername: string]
        ]
    ): Promise<unknown>;
```

* downlevel in a loop, until stabilised

Workaround for sandersn/downlevel-dts#50
  • Loading branch information
mmkal authored Dec 5, 2020
1 parent 59ac012 commit 1c90f91
Show file tree
Hide file tree
Showing 11 changed files with 1,689 additions and 811 deletions.
6 changes: 3 additions & 3 deletions codegen/__tests__/generate-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test("formatOverloads", () => {
*
* [Full docs](https://redis.io/commands/latency-reset)
*/
latency(latency_subcommand: \\"RESET\\", ...event?: Array<string>):
latency(latency_subcommand: \\"RESET\\", ...event: Array<string>):
Promise<unknown>
",
]
Expand Down Expand Up @@ -72,7 +72,7 @@ test("formatOverloads", () => {
*
* [Full docs](https://redis.io/commands/set)
*/
set(key: string, value: string, expiration?: ([(\\"EX\\"|\\"PX\\"), (number)]) | (\\"KEEPTTL\\"), get?: \\"GET\\"):
set(key: string, value: string, expiration?: ([ex_px: (\\"EX\\"|\\"PX\\"), number: (number)]) | (\\"KEEPTTL\\"), get?: \\"GET\\"):
Promise<(\\"OK\\") | (string) | (null)>
",
"
Expand All @@ -84,7 +84,7 @@ test("formatOverloads", () => {
*
* [Full docs](https://redis.io/commands/set)
*/
set(key: string, value: string, expiration?: ([(\\"EX\\"|\\"PX\\"), (number)]) | (\\"KEEPTTL\\"), condition?: \\"NX\\"|\\"XX\\", get?: \\"GET\\"):
set(key: string, value: string, expiration?: ([ex_px: (\\"EX\\"|\\"PX\\"), number: (number)]) | (\\"KEEPTTL\\"), condition?: \\"NX\\"|\\"XX\\", get?: \\"GET\\"):
Promise<(\\"OK\\") | (string) | (null)>
",
]
Expand Down
6 changes: 3 additions & 3 deletions codegen/__tests__/generate-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("toArgs", () => {
Object {
"command": "set",
"context": Array [
"decoding set overload 0 (key,value): { name: 'key', schema: { type: 'string' } },{ name: 'value', schema: { type: 'string' } }",
"decoding set overload 0 (key,value): { name: 'key', schema: { title: 'key', type: 'string' } },{ name: 'value', schema: { title: 'value', type: 'string' } }",
"foo successfully decoded as key (string). Decoded value foo. Tokens remaining [bar,WRONG,123], target args remainin count: 1",
"bar successfully decoded as value (string). Decoded value bar. Tokens remaining [WRONG,123], target args remainin count: 0",
"Tokens remain but no target args left! Tokens: WRONG,123",
Expand Down Expand Up @@ -76,7 +76,7 @@ describe("toArgs", () => {
Object {
"command": "setbit",
"context": Array [
"decoding setbit overload 0 (key,offset,value): { name: 'key', schema: { type: 'string' } },{ name: 'offset', schema: { type: 'integer' } },{ name: 'value', schema: { type: 'integer' } }",
"decoding setbit overload 0 (key,offset,value): { name: 'key', schema: { title: 'key', type: 'string' } },{ name: 'offset', schema: { title: 'offset', type: 'integer' } },{ name: 'value', schema: { title: 'value', type: 'integer' } }",
"foo successfully decoded as key (string). Decoded value foo. Tokens remaining [1.2,34], target args remainin count: 2",
"1.2 isn't an integer. Decoded as something different: 1",
],
Expand All @@ -89,7 +89,7 @@ describe("toArgs", () => {
Object {
"command": "setbit",
"context": Array [
"decoding setbit overload 0 (key,offset,value): { name: 'key', schema: { type: 'string' } },{ name: 'offset', schema: { type: 'integer' } },{ name: 'value', schema: { type: 'integer' } }",
"decoding setbit overload 0 (key,offset,value): { name: 'key', schema: { title: 'key', type: 'string' } },{ name: 'offset', schema: { title: 'offset', type: 'integer' } },{ name: 'value', schema: { title: 'value', type: 'integer' } }",
"foo successfully decoded as key (string). Decoded value foo. Tokens remaining [not_an_integer,34], target args remainin count: 2",
"not_an_integer isn't an integer. Decoded as something different: NaN",
],
Expand Down
5 changes: 3 additions & 2 deletions codegen/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,17 @@ export interface Command {
| "sorted_set";
}

export type ArgumentType = "string" | "key" | "enum" | "pattern" | "integer" | "double" | "posix time";
export type ArgumentType = "string" | "key" | "enum" | "pattern" | "integer" | "double" | "posix time" | "block";

export interface Argument {
name: string;
name?: string;
type: ArgumentType | ArgumentType[];
command?: string;
enum?: string[];
optional?: boolean;
multiple?: boolean;
variadic?: boolean;
block?: Array<Argument>;
}

export interface CommandCollection {
Expand Down
30 changes: 18 additions & 12 deletions codegen/generate-client.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { schema as actualSchema, JsonSchemaCommandArgument, JsonSchemaCommand } from ".";
import { writeFile } from "./util";
import { camelCase, kebabCase, snakeCase } from "lodash";
import * as lo from "lodash";
import * as jsonSchema from "json-schema";
import { fixupClientTypescript } from "./patches/client";

/** occasionally redis-doc includes non-word characters in arg names. Special-case some of them, snakeCase will just throw out the rest */
const santiseArgName = (name: string) =>
lo.snakeCase(name.replace("$", "dollar").replace("*", "asterisk").replace("=", "equals").replace("~", "tilde"));

const codeArgument = (arg: JsonSchemaCommandArgument, i: number, arr: typeof arg[]) => {
let name = snakeCase(arg.name);
let name = santiseArgName(arg.name);
if (name === "arguments") {
name = "args";
}
const type = schemaToTypeScript(arg.schema);
if (type.startsWith("Array<") && i === arr.length - 1) {
const isVarArg = type.startsWith("Array<") && i === arr.length - 1;
if (isVarArg) {
name = "..." + name;
}
const optionalMarker = arr.slice(i).every(a => a.optional) ? "?" : "";
return [name, optionalMarker, ": ", type].join("");
const optionalMarker = !isVarArg && arr.slice(i).every(a => a.optional) ? "?" : "";
return [name || santiseArgName(type), optionalMarker, ": ", type].join("");
};

const formatCodeArguments = (list: JsonSchemaCommandArgument[]) => list.map(codeArgument).join(", ");
Expand All @@ -39,10 +43,12 @@ const schemaToTypeScript = (schema: jsonSchema.JSONSchema7): string => {
}
if (schema.type === "array") {
if (Array.isArray(schema.items)) {
return `[${schema.items
.map(schemaToTypeScript)
.map(t => `(${t})`)
.join(", ")}]`;
const labeled = schema.items.map(item => {
const itemSchema = item as jsonSchema.JSONSchema7;
const ts = schemaToTypeScript(itemSchema);
return `${santiseArgName(itemSchema.title || ts)}: (${ts})`;
});
return `[${labeled.join(", ")}]`;
}
const itemType = typeof schema.items === "object" ? schemaToTypeScript(schema.items) : unknownType;
return `Array<${itemType}>`;
Expand Down Expand Up @@ -86,7 +92,7 @@ export const formatOverloads = (fullCommand: string, { arguments: originalArgs,

const withSubcommands = [
...subCommands.map<typeof originalArgs[0]>((sub, i) => ({
name: snakeCase(`${command}_subcommand${i > 0 ? i + 1 : ""}`),
name: santiseArgName(`${command}_subcommand${i > 0 ? i + 1 : ""}`),
schema: { type: "string", enum: [sub] },
})),
...originalArgs,
Expand Down Expand Up @@ -125,9 +131,9 @@ export const formatOverloads = (fullCommand: string, { arguments: originalArgs,
* - _complexity_: ${spec.complexity}
* - _since_: ${spec.since}
*
* [Full docs](https://redis.io/commands/${kebabCase(fullCommand)})
* [Full docs](https://redis.io/commands/${lo.kebabCase(fullCommand)})
*/
${camelCase(command)}(${val.formatted}):
${lo.camelCase(command)}(${val.formatted}):
Promise<${schemaToTypeScript(spec.return)}>
`;
})
Expand Down
53 changes: 36 additions & 17 deletions codegen/generate-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ import * as commandTypes from "./command";
import { fixupSchema } from "./patches/schema";
import { JsonSchemaCommand } from ".";

const argToSchema = (arg: commandTypes.Argument): jsonSchema.JSONSchema7 => {
const argToSchema: typeof argToSchemaNoTitle = arg => {
const schema = argToSchemaNoTitle(arg);
const name = arg.name || arg.enum?.map?.((e, i, a) => (i > 0 && i === a.length - 1 ? `or ${e}` : e));
return {
title: arg.command || (Array.isArray(name) ? name.join(", ") : name),
...schema,
};
};

const argToSchemaNoTitle = (arg: commandTypes.Argument): jsonSchema.JSONSchema7 => {
if (arg.variadic && arg.command) {
return {
type: "array",
Expand All @@ -22,6 +31,12 @@ const argToSchema = (arg: commandTypes.Argument): jsonSchema.JSONSchema7 => {
items: argToSchema({ ...arg, multiple: false, variadic: false }),
};
}
if (arg.command && !arg.type) {
return {
type: "string",
const: arg.command,
};
}
if (arg.command) {
const { command, ...rest } = arg;
return {
Expand Down Expand Up @@ -78,8 +93,8 @@ const argToSchema = (arg: commandTypes.Argument): jsonSchema.JSONSchema7 => {
return {
type: "array",
items: arg.type.map((type, i) => ({
title: arg.name[i],
...argToSchema({ type, name: arg.name[i] }),
title: arg.name?.[i],
...argToSchema({ type, name: arg.name?.[i] }),
})),
};
}
Expand Down Expand Up @@ -139,20 +154,24 @@ const argToReturn = (command: string): jsonSchema.JSONSchema7 => {

const jsonSchemaCommand = (command: commandTypes.Command, key: string): JsonSchemaCommand => ({
...command,
arguments: (command?.arguments || []).map(arg => ({
name: [arg.command, arg.name]
.flat()
.filter(
(val, i, arr) =>
val &&
val !== arr[i - 1] &&
val.toUpperCase() !== arr[i - 1] &&
val.toUpperCase() + "S" !== arr[i - 1]
)
.join("_"),
optional: arg.optional,
schema: argToSchema(arg),
})),
arguments: (command?.arguments || []).map(arg => {
const schema = argToSchema(arg);
return {
name: [arg.command, schema.title || arg.name]
.flat()
.filter((val, i, arr) => val && !arr[i + 1]?.toUpperCase().startsWith(val.toUpperCase()))
.filter(
(val, i, arr) =>
val &&
val !== arr[i - 1] &&
val.toUpperCase() !== arr[i - 1] &&
val.toUpperCase() + "S" !== arr[i - 1]
)
.join("_"),
optional: arg.optional,
schema,
};
}),
return: argToReturn(key),
});

Expand Down
Loading

0 comments on commit 1c90f91

Please sign in to comment.