Skip to content

Commit d87911a

Browse files
committed
Basic implementation of command manager.
1 parent 77dd9bf commit d87911a

2 files changed

Lines changed: 155 additions & 6 deletions

File tree

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,47 @@
1-
export class CommandManager {}
1+
export interface Disposable {
2+
dispose(): void;
3+
}
4+
5+
export type CommandFunction = (...args: any[]) => Promise<unknown>;
6+
7+
export class CommandManager<
8+
Commands extends Record<string, CommandFunction>,
9+
CommandName extends keyof Commands & string = keyof Commands & string,
10+
> implements Disposable
11+
{
12+
// TODO: should this be a map?
13+
// TODO: handle multiple command names
14+
private commands: Disposable[] = [];
15+
16+
constructor(
17+
private readonly commandRegister: <T extends CommandName>(
18+
commandName: T,
19+
definition: Commands[T],
20+
) => Disposable,
21+
private readonly commandExecute: <T extends CommandName>(
22+
commandName: T,
23+
...args: Parameters<Commands[T]>
24+
) => Promise<ReturnType<Commands[T]>>,
25+
) {}
26+
27+
registerCommand<T extends CommandName>(
28+
commandName: T,
29+
definition: Commands[T],
30+
): void {
31+
this.commands.push(this.commandRegister(commandName, definition));
32+
}
33+
34+
executeCommand<T extends CommandName>(
35+
commandName: T,
36+
...args: Parameters<Commands[T]>
37+
): Promise<ReturnType<Commands[T]>> {
38+
return this.commandExecute(commandName, ...args);
39+
}
40+
41+
dispose(): void {
42+
this.commands.forEach((cmd) => {
43+
cmd.dispose();
44+
});
45+
this.commands = [];
46+
}
47+
}
Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,111 @@
1-
import { CommandManager } from "../../../../src/packages/commands";
1+
import {
2+
CommandFunction,
3+
CommandManager,
4+
} from "../../../../src/packages/commands";
25

3-
describe(CommandManager.name, () => {
4-
it("can create a command manager", () => {
5-
const commandManager = new CommandManager();
6-
expect(commandManager).not.toBeUndefined();
6+
describe("CommandManager", () => {
7+
it("can register a command", () => {
8+
const commandRegister = jest.fn();
9+
const commandManager = new CommandManager<Record<string, CommandFunction>>(
10+
commandRegister,
11+
jest.fn(),
12+
);
13+
const myCommand = jest.fn();
14+
commandManager.registerCommand("abc", myCommand);
15+
expect(commandRegister).toHaveBeenCalledTimes(1);
16+
expect(commandRegister).toHaveBeenCalledWith("abc", myCommand);
17+
});
18+
19+
it("can register typed commands", async () => {
20+
const commands = {
21+
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
22+
return variantAnalysisId * 10;
23+
},
24+
};
25+
const commandManager = new CommandManager<typeof commands>(
26+
jest.fn(),
27+
jest.fn(),
28+
);
29+
30+
// @ts-expect-error wrong command name should give a type error
31+
commandManager.registerCommand("abc", jest.fn());
32+
33+
commandManager.registerCommand(
34+
"codeQL.openVariantAnalysisLogs",
35+
// @ts-expect-error wrong function parameter type should give a type error
36+
async (variantAnalysisId: string): Promise<number> => 10,
37+
);
38+
39+
commandManager.registerCommand(
40+
"codeQL.openVariantAnalysisLogs",
41+
// @ts-expect-error wrong function return type should give a type error
42+
async (variantAnalysisId: number): Promise<string> => "hello",
43+
);
44+
45+
// Working types
46+
commandManager.registerCommand(
47+
"codeQL.openVariantAnalysisLogs",
48+
async (variantAnalysisId: number): Promise<number> =>
49+
variantAnalysisId * 10,
50+
);
51+
});
52+
53+
it("can dispose of its commands", () => {
54+
const dispose1 = jest.fn();
55+
const dispose2 = jest.fn();
56+
const commandRegister = jest
57+
.fn()
58+
.mockReturnValueOnce({ dispose: dispose1 })
59+
.mockReturnValueOnce({ dispose: dispose2 });
60+
const commandManager = new CommandManager<Record<string, CommandFunction>>(
61+
commandRegister,
62+
jest.fn(),
63+
);
64+
commandManager.registerCommand("abc", jest.fn());
65+
commandManager.registerCommand("def", jest.fn());
66+
expect(dispose1).not.toHaveBeenCalled();
67+
expect(dispose2).not.toHaveBeenCalled();
68+
commandManager.dispose();
69+
expect(dispose1).toHaveBeenCalledTimes(1);
70+
expect(dispose2).toHaveBeenCalledTimes(1);
71+
});
72+
73+
it("can execute a command", async () => {
74+
const commandExecute = jest.fn().mockReturnValue(7);
75+
const commandManager = new CommandManager<Record<string, CommandFunction>>(
76+
jest.fn(),
77+
commandExecute,
78+
);
79+
const result = await commandManager.executeCommand("abc", "hello", true);
80+
expect(result).toEqual(7);
81+
expect(commandExecute).toHaveBeenCalledTimes(1);
82+
expect(commandExecute).toHaveBeenCalledWith("abc", "hello", true);
83+
});
84+
85+
it("can execute typed commands", async () => {
86+
const commands = {
87+
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
88+
return variantAnalysisId * 10;
89+
},
90+
};
91+
const commandManager = new CommandManager<typeof commands>(
92+
jest.fn(),
93+
jest.fn(),
94+
);
95+
96+
// @ts-expect-error wrong command name should give a type error
97+
await commandManager.executeCommand("abc", 4);
98+
99+
await commandManager.executeCommand(
100+
"codeQL.openVariantAnalysisLogs",
101+
// @ts-expect-error wrong argument type should give a type error
102+
"xyz",
103+
);
104+
105+
// @ts-expect-error wrong number of arguments should give a type error
106+
await commandManager.executeCommand("codeQL.openVariantAnalysisLogs", 2, 3);
107+
108+
// Working types
109+
await commandManager.executeCommand("codeQL.openVariantAnalysisLogs", 7);
7110
});
8111
});

0 commit comments

Comments
 (0)