Skip to content

Commit 2c53fbe

Browse files
committed
Add spec for the BotCommandHandler
1 parent e111f6b commit 2c53fbe

File tree

2 files changed

+145
-13
lines changed

2 files changed

+145
-13
lines changed

spec/unit/bot-command.spec.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import "jasmine";
2+
import { ActivityTracker, BotCommand, BotCommandHandler, CommandArguments } from "../../src/index";
3+
import { WhoisInfo, PresenceEventContent, MatrixClient } from "matrix-bot-sdk";
4+
5+
6+
describe("BotCommands", () => {
7+
it("does not construct without commands", () => {
8+
expect(() => new BotCommandHandler({}, undefined)).toThrowError('Prototype did not have any bot commands bound');
9+
});
10+
11+
it("to process a simple command", async () => {
12+
let called = false;
13+
14+
class SimpleBotCommander {
15+
@BotCommand({ help: "Some help", name: "simple-command"})
16+
public simpleCommand(data: CommandArguments<null>): void {
17+
called = true;
18+
}
19+
}
20+
21+
const handler = new BotCommandHandler(new SimpleBotCommander());
22+
await handler.handleCommand("simple-command", null);
23+
expect(called).toBeTrue();
24+
});
25+
26+
it("to process a simple command with augments", async () => {
27+
let called: any = undefined;
28+
29+
class SimpleBotCommander {
30+
@BotCommand({ help: "Some help", name: "simple-command", requiredArgs: ["foo", "bar"]})
31+
public simpleCommand(data: CommandArguments<{some: string}>): void {
32+
called = data;
33+
}
34+
}
35+
36+
const handler = new BotCommandHandler(new SimpleBotCommander());
37+
await handler.handleCommand("simple-command abc def", {some: "context"});
38+
const expectedResult = {
39+
args: ["abc", "def"],
40+
request: {
41+
some: "context",
42+
}
43+
}
44+
expect(called).toEqual(expectedResult);
45+
});
46+
47+
it("to process a simple command with optional parameters", async () => {
48+
let called: any = undefined;
49+
50+
class SimpleBotCommander {
51+
@BotCommand({ help: "Some help", name: "simple-command", requiredArgs: ["foo", "bar"], optionalArgs: ["baz"]})
52+
public simpleCommand(data: CommandArguments<{some: string}>): void {
53+
called = data;
54+
}
55+
}
56+
57+
const handler = new BotCommandHandler(new SimpleBotCommander());
58+
await handler.handleCommand("simple-command abc def", {some: "context"});
59+
expect(called).toEqual({
60+
args: ["abc", "def"],
61+
request: {
62+
some: "context",
63+
}
64+
});
65+
66+
await handler.handleCommand("simple-command abc def ghi", {some: "context"});
67+
expect(called).toEqual({
68+
args: ["abc", "def", "ghi"],
69+
request: {
70+
some: "context",
71+
}
72+
});
73+
});
74+
75+
it("to process a command and a subcommand", async () => {
76+
let commandCalled: any = undefined;
77+
let subCommandCalled: any = undefined;
78+
79+
class SimpleBotCommander {
80+
@BotCommand({ help: "Some help", name: "simple-command", requiredArgs: ["foo"]})
81+
public simpleCommand(data: CommandArguments<{some: string}>): void {
82+
commandCalled = data;
83+
}
84+
@BotCommand({ help: "Some help", name: "simple-command with-a-subcommand", requiredArgs: ["foo"]})
85+
public simpleSubCommand(data: CommandArguments<{some: string}>): void {
86+
subCommandCalled = data;
87+
}
88+
}
89+
90+
const handler = new BotCommandHandler(new SimpleBotCommander());
91+
await handler.handleCommand("simple-command abc", undefined);
92+
expect(commandCalled).toEqual({
93+
args: ["abc"],
94+
request: undefined,
95+
});
96+
97+
await handler.handleCommand("simple-command with-a-subcommand def", undefined);
98+
expect(subCommandCalled).toEqual({
99+
args: ["def"],
100+
request: undefined,
101+
});
102+
});
103+
104+
it("should produce useful help output", async () => {
105+
class SimpleBotCommander {
106+
@BotCommand({ help: "No help at all", name: "very-simple-command"})
107+
public verySimpleCommand(data: CommandArguments<{some: string}>): void {
108+
}
109+
@BotCommand({ help: "Some help", name: "simple-command", requiredArgs: ["requiredArg1"]})
110+
public simpleCommand(data: CommandArguments<{some: string}>): void {
111+
}
112+
@BotCommand({ help: "Even better help", name: "simple-command with-a-subcommand", requiredArgs: ["requiredArg1"], optionalArgs: ["optionalArg1"]})
113+
public simpleSubCommand(data: CommandArguments<{some: string}>): void {
114+
}
115+
}
116+
117+
const handler = new BotCommandHandler(new SimpleBotCommander());
118+
expect(handler.helpMessage.format).toEqual("org.matrix.custom.html");
119+
expect(handler.helpMessage.msgtype).toEqual("m.notice");
120+
expect(handler.helpMessage.body).toContain("Commands:");
121+
// Rough formatting match
122+
expect(handler.helpMessage.body).toContain("- `very-simple-command` - No help at all");
123+
expect(handler.helpMessage.body).toContain("- `simple-command` requiredArg1 - Some help");
124+
expect(handler.helpMessage.body).toContain("- `simple-command with-a-subcommand` requiredArg1 [optionalArg1] - Even better help");
125+
});
126+
});
127+

src/components/bot-commands.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ const md = new Markdown();
77

88
interface BotCommandEntry<R> {
99
fn: BotCommandFunction<R>;
10-
requiredArgs: string[];
10+
requiredArgs?: string[];
1111
optionalArgs?: string[];
1212
}
1313

1414
interface BotCommandMetadata {
1515
help: string;
1616
name: string;
17-
requiredArgs: string[],
17+
requiredArgs?: string[],
1818
optionalArgs?: string[],
1919
}
2020

@@ -25,8 +25,9 @@ const botCommandSymbol = Symbol("botCommandMetadata");
2525
* `CommandArguments` parameter.
2626
* @param options Metadata about the command.
2727
*/
28-
export function BotCommand(options: BotCommandMetadata): void {
29-
Reflect.metadata(botCommandSymbol, options);
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
export function BotCommand(options: BotCommandMetadata): any {
30+
return Reflect.metadata(botCommandSymbol, options);
3031
}
3132
export interface CommandArguments<R> {
3233
request: R;
@@ -54,7 +55,9 @@ export class BotCommandError extends Error {
5455
}
5556
}
5657

57-
export class BotCommandHandler<T, R extends Record<string, unknown>> {
58+
// Typescript doesn't understand that classes are indexable.
59+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
60+
export class BotCommandHandler<T extends Record<string, any>, R extends Record<string, unknown>|null|undefined> {
5861
/**
5962
* The body of a Matrix message to be sent to users when they ask for help.
6063
*/
@@ -70,23 +73,25 @@ export class BotCommandHandler<T, R extends Record<string, unknown>> {
7073
* should **include** any whitspace E.g. `!irc `.
7174
*/
7275
constructor(
73-
prototype: Record<string, BotCommandFunction<R>>,
7476
instance: T,
7577
private readonly prefix?: string) {
7678
let content = "Commands:\n";
7779
const botCommands: {[prefix: string]: BotCommandEntry<R>} = {};
78-
Object.getOwnPropertyNames(prototype).forEach(propetyKey => {
79-
const b = Reflect.getMetadata(botCommandSymbol, prototype, propetyKey) as BotCommandMetadata;
80+
const proto = Object.getPrototypeOf(instance);
81+
Object.getOwnPropertyNames(proto).forEach(propetyKey => {
82+
const b = Reflect.getMetadata(botCommandSymbol, instance, propetyKey) as BotCommandMetadata;
8083
if (!b) {
8184
// Not a bot command function.
8285
return;
8386
}
84-
const requiredArgs = b.requiredArgs.join(" ");
85-
const optionalArgs = b.optionalArgs?.map((arg: string) => `[${arg}]`).join(" ") || "";
86-
content += ` - \`${this.prefix || ""}${b.name}\` ${requiredArgs} ${optionalArgs} - ${b.help}\n`;
87+
const optionalArgs = b.optionalArgs?.map((arg: string) => `[${arg}]`) || [];
88+
const args = [...(b.requiredArgs || []), ...optionalArgs].join(" ");
89+
90+
content += ` - \`${this.prefix || ""}${b.name}\`${args && " "}${args} - ${b.help}\n`;
8791
// We know that this is safe.
92+
const fn = instance[propetyKey];
8893
botCommands[b.name as string] = {
89-
fn: prototype[propetyKey].bind(instance),
94+
fn: fn.bind(instance),
9095
requiredArgs: b.requiredArgs,
9196
optionalArgs: b.optionalArgs,
9297
};
@@ -143,7 +148,7 @@ export class BotCommandHandler<T, R extends Record<string, unknown>> {
143148
continue;
144149
}
145150
// We have a match!
146-
if (command.requiredArgs.length > parts.length - i) {
151+
if ((command.requiredArgs?.length || 0) > parts.length - i) {
147152
throw new BotCommandError("Missing arguments", "Missing required arguments for this command");
148153
}
149154
await command.fn({

0 commit comments

Comments
 (0)