Skip to content

Commit 32d591b

Browse files
authored
feat(arguments): extract logic to resolvers (#237)
This extract the logic of all Arguments to public resolver functions, exported under the Resolvers namespace Other changes: - Ability to add custom values on the fly via context for BooleanArgument - Add tests for some resolvers - For arguments where a guild is required, if no guild is found, guild won't be passed to the context anymore - For arguments where a guild is required, if a guild is found but a resolver error occurs, the guild will be passed to the context Co-authored-by: Antonio Román <[email protected]> Co-authored-by: Jeroen Claassens <[email protected]> BREAKING CHANGE: Changed the error message of DateArgument BREAKING CHANGE: Changed the error message of FloatArgument BREAKING CHANGE: Changed the error message of NumberArgument BREAKING CHANGE: Changed the error message of IntegerArgument BREAKING CHANGE: Changed the error message of all arguments that must be run in a guild BREAKING CHANGE: Changed the error message of GuildNewsThreadChannelArgument BREAKING CHANGE: Changed the error message of GuildPrivateThreadChannelArgument BREAKING CHANGE: Changed the error message of GuildPublicThreadChannelArgument BREAKING CHANGE: Changed the error message of GuildStageVoiceChannelArgument BREAKING CHANGE: Changed the error message of GuildTextChannelArgument BREAKING CHANGE: Changed the error message of GuildThreadChannelArgument BREAKING CHANGE: Changed the error message of GuildVoiceChannelArgument BREAKING CHANGE: Changed the error message of GuildMemberArgument BREAKING CHANGE: Changed the error message of UserArgument BREAKING CHANGE: Made MessageArgumentContext private BREAKING CHANGE: Stop exposing the channel property in context of the ChannelArgument error BREAKING CHANGE: Stop exposing the channel property in context of the GuildCategoryChannelArgument error BREAKING CHANGE: Stop exposing the channel property in context of the GuildNewsChannelArgument error BREAKING CHANGE: Stop exposing the channel property in context of the GuildPrivateThreadArgument error BREAKING CHANGE: Stop exposing the channel property in context of the GuildStageVoiceChannelArgument error BREAKING CHANGE: Stop exposing the channel property in context of the GuildTextChannelArgument error BREAKING CHANGE: Stop exposing the channel property in context of the GuildThreadChannelArgument error BREAKING CHANGE: Stop exposing the channel property in context of the GuildVoiceChannelArgument error BREAKING CHANGE: Rename Identifiers.ArgumentBoolean to Identifiers.ArgumentBooleanError BREAKING CHANGE: Rename Identifiers.ArgumentCategoryChannel to Identifiers.ArgumentGuildCategoryChannelError BREAKING CHANGE: Rename Identifiers.ArgumentChannel to Identifiers.ArgumentChannelError BREAKING CHANGE: Rename Identifiers.ArgumentDate to Identifiers.ArgumentDateError BREAKING CHANGE: Rename Identifiers.ArgumentDateTooSmall to Identifiers.ArgumentDateTooEarly BREAKING CHANGE: Rename Identifiers.ArgumentDateTooBig to Identifiers.ArgumentDateTooFar BREAKING CHANGE: Rename Identifiers.ArgumentDMChannel to Identifiers.ArgumentDMChannelError BREAKING CHANGE: Rename Identifiers.ArgumentFloat to Identifiers.ArgumentFloatError BREAKING CHANGE: Rename Identifiers.ArgumentFloatTooBig to Identifiers.ArgumentFloatTooLarge BREAKING CHANGE: Rename Identifiers.ArgumentGuildChannel to Identifiers.ArgumentGuildChannelError BREAKING CHANGE: Rename Identifiers.ArgumentGuildChannelMissingGuild to Identifiers.ArgumentGuildChannelMissingGuildError BREAKING CHANGE: Rename Identifiers.ArgumentHyperlink to Identifiers.ArgumentHyperlinkError BREAKING CHANGE: Rename Identifiers.ArgumentInteger to Identifiers.ArgumentIntegerError BREAKING CHANGE: Rename Identifiers.ArgumentIntegerTooBig to Identifiers.ArgumentIntegerTooLarge BREAKING CHANGE: Rename Identifiers.ArgumentMember to Identifiers.ArgumentMemberError BREAKING CHANGE: Rename Identifiers.ArgumentMessage to Identifiers.ArgumentMessageError BREAKING CHANGE: Rename Identifiers.ArgumentNewsChannel to Identifiers.ArgumentGuildNewsChannelError BREAKING CHANGE: Rename Identifiers.ArgumentNumber to Identifiers.ArgumentNumberError BREAKING CHANGE: Rename Identifiers.ArgumentNumberTooBig to Identifiers.ArgumentNumberTooLarge BREAKING CHANGE: Rename Identifiers.ArgumentRole to Identifiers.ArgumentRoleError BREAKING CHANGE: Rename Identifiers.ArgumentTextChannel to Identifiers.ArgumentGuildTextChannel BREAKING CHANGE: Rename Identifiers.ArgumentUser to Identifiers.ArgumentUserError BREAKING CHANGE: Rename Identifiers.ArgumentVoiceChannel to Identifiers.ArgumentGuildVoiceChannel
1 parent b9c36de commit 32d591b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1174
-463
lines changed

src/arguments/CoreBoolean.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import type { PieceContext } from '@sapphire/pieces';
2+
import { resolveBoolean } from '../lib/resolvers';
23
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
34

4-
const truths = ['1', 'true', '+', 't', 'yes', 'y'];
5-
const falses = ['0', 'false', '-', 'f', 'no', 'n'];
6-
75
export class CoreArgument extends Argument<boolean> {
86
public constructor(context: PieceContext) {
97
super(context, { name: 'boolean' });
108
}
119

12-
public run(parameter: string, context: ArgumentContext): ArgumentResult<boolean> {
13-
const boolean = parameter.toLowerCase();
14-
if (truths.includes(boolean)) return this.ok(true);
15-
if (falses.includes(boolean)) return this.ok(false);
16-
17-
return this.error({ parameter, message: 'The argument did not resolve to a boolean.', context });
10+
public run(parameter: string, context: { truths?: string[]; falses?: string[] } & ArgumentContext): ArgumentResult<boolean> {
11+
const resolved = resolveBoolean(parameter, { truths: context.truths, falses: context.falses });
12+
if (resolved.success) return this.ok(resolved.value);
13+
return this.error({
14+
parameter,
15+
identifier: resolved.error,
16+
message: 'The argument did not resolve to a boolean.',
17+
context
18+
});
1819
}
1920
}

src/arguments/CoreChannel.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1+
import type { ChannelTypes } from '@sapphire/discord.js-utilities';
12
import type { PieceContext } from '@sapphire/pieces';
2-
import type { Channel, Snowflake } from 'discord.js';
3+
import { resolveChannel } from '../lib/resolvers';
34
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
45

5-
export class CoreArgument extends Argument<Channel> {
6+
export class CoreArgument extends Argument<ChannelTypes> {
67
public constructor(context: PieceContext) {
78
super(context, { name: 'channel' });
89
}
910

10-
public run(parameter: string, context: ArgumentContext): ArgumentResult<Channel> {
11-
const channel = (context.message.guild ? context.message.guild.channels : this.container.client.channels).cache.get(parameter as Snowflake);
12-
return channel
13-
? this.ok(channel)
14-
: this.error({
15-
parameter,
16-
message: 'The argument did not resolve to a channel.',
17-
context: { ...context, channel }
18-
});
11+
public run(parameter: string, context: ArgumentContext): ArgumentResult<ChannelTypes> {
12+
const resolved = resolveChannel(parameter, context.message);
13+
if (resolved.success) return this.ok(resolved.value);
14+
return this.error({
15+
parameter,
16+
identifier: resolved.error,
17+
message: 'The argument did not resolve to a channel.',
18+
context
19+
});
1920
}
2021
}

src/arguments/CoreDMChannel.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import { ChannelTypes, isDMChannel } from '@sapphire/discord.js-utilities';
21
import type { PieceContext } from '@sapphire/pieces';
32
import type { DMChannel } from 'discord.js';
4-
import type { ArgumentResult } from '../lib/structures/Argument';
5-
import { ExtendedArgument, ExtendedArgumentContext } from '../lib/structures/ExtendedArgument';
3+
import { resolveDMChannel } from '../lib/resolvers';
4+
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
65

7-
export class CoreArgument extends ExtendedArgument<'channel', DMChannel> {
6+
export class CoreArgument extends Argument<DMChannel> {
87
public constructor(context: PieceContext) {
9-
super(context, { baseArgument: 'channel', name: 'dmChannel' });
8+
super(context, { name: 'dmChannel' });
109
}
1110

12-
public handle(channel: ChannelTypes, context: ExtendedArgumentContext): ArgumentResult<DMChannel> {
13-
return isDMChannel(channel)
14-
? this.ok(channel)
15-
: this.error({
16-
parameter: context.parameter,
17-
message: 'The argument did not resolve to a DM channel.',
18-
context: { ...context, channel }
19-
});
11+
public run(parameter: string, context: ArgumentContext): ArgumentResult<DMChannel> {
12+
const resolved = resolveDMChannel(parameter, context.message);
13+
if (resolved.success) return this.ok(resolved.value);
14+
return this.error({
15+
parameter,
16+
identifier: resolved.error,
17+
message: 'The argument did not resolve to a DM channel.',
18+
context
19+
});
2020
}
2121
}

src/arguments/CoreDate.ts

+15-20
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,27 @@
11
import type { PieceContext } from '@sapphire/pieces';
22
import { Identifiers } from '../lib/errors/Identifiers';
3+
import { resolveDate } from '../lib/resolvers';
34
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
45

56
export class CoreArgument extends Argument<Date> {
7+
private readonly messages = {
8+
[Identifiers.ArgumentDateTooEarly]: ({ minimum }: ArgumentContext) => `The given date must be after ${new Date(minimum!).toISOString()}.`,
9+
[Identifiers.ArgumentDateTooFar]: ({ maximum }: ArgumentContext) => `The given date must be before ${new Date(maximum!).toISOString()}.`,
10+
[Identifiers.ArgumentDateError]: () => 'The argument did not resolve to a date.'
11+
} as const;
12+
613
public constructor(context: PieceContext) {
714
super(context, { name: 'date' });
815
}
916

1017
public run(parameter: string, context: ArgumentContext): ArgumentResult<Date> {
11-
const parsed = new Date(parameter);
12-
const time = parsed.getTime();
13-
14-
if (Number.isNaN(time)) {
15-
return this.error({
16-
parameter,
17-
message: 'The argument did not resolve to a valid date.',
18-
context
19-
});
20-
}
21-
22-
if (typeof context.minimum === 'number' && time < context.minimum) {
23-
return this.error({ parameter, identifier: Identifiers.ArgumentDateTooSmall, message: 'The argument is too small.', context });
24-
}
25-
26-
if (typeof context.maximum === 'number' && time > context.maximum) {
27-
return this.error({ parameter, identifier: Identifiers.ArgumentDateTooBig, message: 'The argument is too big.', context });
28-
}
29-
30-
return this.ok(parsed);
18+
const resolved = resolveDate(parameter, { minimum: context.minimum, maximum: context.maximum });
19+
if (resolved.success) return this.ok(resolved.value);
20+
return this.error({
21+
parameter,
22+
identifier: resolved.error,
23+
message: this.messages[resolved.error](context),
24+
context
25+
});
3126
}
3227
}

src/arguments/CoreFloat.ts

+15-25
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,27 @@
11
import type { PieceContext } from '@sapphire/pieces';
22
import { Identifiers } from '../lib/errors/Identifiers';
3+
import { resolveFloat } from '../lib/resolvers';
34
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
45

56
export class CoreArgument extends Argument<number> {
7+
private readonly messages = {
8+
[Identifiers.ArgumentFloatTooSmall]: ({ minimum }: ArgumentContext) => `The given number must be greater than ${minimum}.`,
9+
[Identifiers.ArgumentFloatTooLarge]: ({ maximum }: ArgumentContext) => `The given number must be less than ${maximum}.`,
10+
[Identifiers.ArgumentFloatError]: () => 'The argument did not resolve to a valid decimal.'
11+
} as const;
12+
613
public constructor(context: PieceContext) {
714
super(context, { name: 'float' });
815
}
916

1017
public run(parameter: string, context: ArgumentContext): ArgumentResult<number> {
11-
const parsed = Number(parameter);
12-
13-
if (Number.isNaN(parsed)) {
14-
return this.error({ parameter, message: 'The argument did not resolve to a valid floating point number.', context });
15-
}
16-
17-
if (typeof context.minimum === 'number' && parsed < context.minimum) {
18-
return this.error({
19-
parameter,
20-
identifier: Identifiers.ArgumentFloatTooSmall,
21-
message: `The argument must be greater than ${context.minimum}.`,
22-
context
23-
});
24-
}
25-
26-
if (typeof context.maximum === 'number' && parsed > context.maximum) {
27-
return this.error({
28-
parameter,
29-
identifier: Identifiers.ArgumentFloatTooBig,
30-
message: `The argument must be less than ${context.maximum}.`,
31-
context
32-
});
33-
}
34-
35-
return this.ok(parsed);
18+
const resolved = resolveFloat(parameter, { minimum: context.minimum, maximum: context.maximum });
19+
if (resolved.success) return this.ok(resolved.value);
20+
return this.error({
21+
parameter,
22+
identifier: resolved.error,
23+
message: this.messages[resolved.error](context),
24+
context
25+
});
3626
}
3727
}
+24-16
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
1-
import { GuildBasedChannelTypes, isCategoryChannel } from '@sapphire/discord.js-utilities';
21
import type { PieceContext } from '@sapphire/pieces';
32
import type { CategoryChannel } from 'discord.js';
4-
import type { ArgumentResult } from '../lib/structures/Argument';
5-
import { ExtendedArgument, ExtendedArgumentContext } from '../lib/structures/ExtendedArgument';
3+
import { Identifiers } from '../lib/errors/Identifiers';
4+
import { resolveGuildCategoryChannel } from '../lib/resolvers';
5+
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
66

7-
export class CoreArgument extends ExtendedArgument<'guildChannel', CategoryChannel> {
7+
export class CoreArgument extends Argument<CategoryChannel> {
88
public constructor(context: PieceContext) {
9-
super(context, {
10-
name: 'guildCategoryChannel',
11-
baseArgument: 'guildChannel'
12-
});
9+
super(context, { name: 'guildCategoryChannel' });
1310
}
1411

15-
public handle(channel: GuildBasedChannelTypes, context: ExtendedArgumentContext): ArgumentResult<CategoryChannel> {
16-
return isCategoryChannel(channel)
17-
? this.ok(channel)
18-
: this.error({
19-
parameter: context.parameter,
20-
message: 'The argument did not resolve to a server category channel.',
21-
context: { ...context, channel }
22-
});
12+
public run(parameter: string, context: ArgumentContext): ArgumentResult<CategoryChannel> {
13+
const { guild } = context.message;
14+
if (!guild) {
15+
return this.error({
16+
parameter,
17+
identifier: Identifiers.ArgumentGuildChannelMissingGuildError,
18+
message: 'This command can only be used in a server.',
19+
context
20+
});
21+
}
22+
23+
const resolved = resolveGuildCategoryChannel(parameter, guild);
24+
if (resolved.success) return this.ok(resolved.value);
25+
return this.error({
26+
parameter,
27+
identifier: resolved.error,
28+
message: 'The argument did not resolve to a valid server category channel.',
29+
context: { ...context, guild }
30+
});
2331
}
2432
}

src/arguments/CoreGuildChannel.ts

+12-23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { ChannelMentionRegex, SnowflakeRegex } from '@sapphire/discord-utilities';
21
import type { GuildBasedChannelTypes } from '@sapphire/discord.js-utilities';
32
import type { PieceContext } from '@sapphire/pieces';
4-
import type { Guild, Snowflake } from 'discord.js';
53
import { Identifiers } from '../lib/errors/Identifiers';
4+
import { resolveGuildChannel } from '../lib/resolvers';
65
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
76

87
export class CoreArgument extends Argument<GuildBasedChannelTypes> {
@@ -15,29 +14,19 @@ export class CoreArgument extends Argument<GuildBasedChannelTypes> {
1514
if (!guild) {
1615
return this.error({
1716
parameter,
18-
identifier: Identifiers.ArgumentGuildChannelMissingGuild,
19-
message: 'The argument must be run in a guild.',
20-
context: { ...context, guild }
17+
identifier: Identifiers.ArgumentGuildChannelMissingGuildError,
18+
message: 'This command can only be used in a server.',
19+
context
2120
});
2221
}
2322

24-
const channel = this.resolveById(parameter, guild) ?? this.resolveByQuery(parameter, guild);
25-
return channel
26-
? this.ok(channel)
27-
: this.error({
28-
parameter,
29-
message: 'The argument did not resolve to a guild channel.',
30-
context: { ...context, guild }
31-
});
32-
}
33-
34-
private resolveById(argument: string, guild: Guild): GuildBasedChannelTypes | null {
35-
const channelId = ChannelMentionRegex.exec(argument) ?? SnowflakeRegex.exec(argument);
36-
return channelId ? (guild.channels.cache.get(channelId[1] as Snowflake) as GuildBasedChannelTypes) ?? null : null;
37-
}
38-
39-
private resolveByQuery(argument: string, guild: Guild): GuildBasedChannelTypes | null {
40-
const lowerCaseArgument = argument.toLowerCase();
41-
return (guild.channels.cache.find((channel) => channel.name.toLowerCase() === lowerCaseArgument) as GuildBasedChannelTypes) ?? null;
23+
const resolved = resolveGuildChannel(parameter, guild);
24+
if (resolved.success) return this.ok(resolved.value);
25+
return this.error({
26+
parameter,
27+
identifier: resolved.error,
28+
message: 'The argument did not resolve to a valid server channel.',
29+
context: { ...context, guild }
30+
});
4231
}
4332
}

src/arguments/CoreGuildNewsChannel.ts

+24-16
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
1-
import { GuildBasedChannelTypes, isNewsChannel } from '@sapphire/discord.js-utilities';
21
import type { PieceContext } from '@sapphire/pieces';
32
import type { NewsChannel } from 'discord.js';
4-
import type { ArgumentResult } from '../lib/structures/Argument';
5-
import { ExtendedArgument, ExtendedArgumentContext } from '../lib/structures/ExtendedArgument';
3+
import { Identifiers } from '../lib/errors/Identifiers';
4+
import { resolveGuildNewsChannel } from '../lib/resolvers';
5+
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
66

7-
export class CoreArgument extends ExtendedArgument<'guildChannel', NewsChannel> {
7+
export class CoreArgument extends Argument<NewsChannel> {
88
public constructor(context: PieceContext) {
9-
super(context, {
10-
name: 'guildNewsChannel',
11-
baseArgument: 'guildChannel'
12-
});
9+
super(context, { name: 'guildNewsChannel' });
1310
}
1411

15-
public handle(channel: GuildBasedChannelTypes, context: ExtendedArgumentContext): ArgumentResult<NewsChannel> {
16-
return isNewsChannel(channel)
17-
? this.ok(channel)
18-
: this.error({
19-
parameter: context.parameter,
20-
message: 'The argument did not resolve to a server announcement channel.',
21-
context: { ...context, channel }
22-
});
12+
public run(parameter: string, context: ArgumentContext): ArgumentResult<NewsChannel> {
13+
const { guild } = context.message;
14+
if (!guild) {
15+
return this.error({
16+
parameter,
17+
identifier: Identifiers.ArgumentGuildChannelMissingGuildError,
18+
message: 'This command can only be used in a server.',
19+
context
20+
});
21+
}
22+
23+
const resolved = resolveGuildNewsChannel(parameter, guild);
24+
if (resolved.success) return this.ok(resolved.value);
25+
return this.error({
26+
parameter,
27+
identifier: resolved.error,
28+
message: 'The given argument did not resolve to a valid announcements channel.',
29+
context: { ...context, guild }
30+
});
2331
}
2432
}
+24-15
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
import type { PieceContext } from '@sapphire/pieces';
22
import type { ThreadChannel } from 'discord.js';
3-
import type { ArgumentResult } from '../lib/structures/Argument';
4-
import { ExtendedArgument, ExtendedArgumentContext } from '../lib/structures/ExtendedArgument';
3+
import { Identifiers } from '../lib/errors/Identifiers';
4+
import { resolveGuildNewsThreadChannel } from '../lib/resolvers';
5+
import { Argument, ArgumentContext, ArgumentResult } from '../lib/structures/Argument';
56

6-
export class CoreArgument extends ExtendedArgument<'guildThreadChannel', ThreadChannel> {
7+
export class CoreArgument extends Argument<ThreadChannel> {
78
public constructor(context: PieceContext) {
8-
super(context, {
9-
name: 'guildNewsThreadChannel',
10-
baseArgument: 'guildThreadChannel'
11-
});
9+
super(context, { name: 'guildNewsThreadChannel' });
1210
}
1311

14-
public handle(channel: ThreadChannel, context: ExtendedArgumentContext): ArgumentResult<ThreadChannel> {
15-
return channel.type === 'GUILD_NEWS_THREAD'
16-
? this.ok(channel)
17-
: this.error({
18-
parameter: context.parameter,
19-
message: 'The argument did not resolve to a server announcement thread channel.',
20-
context: { ...context, channel }
21-
});
12+
public run(parameter: string, context: ArgumentContext): ArgumentResult<ThreadChannel> {
13+
const { guild } = context.message;
14+
if (!guild) {
15+
return this.error({
16+
parameter,
17+
identifier: Identifiers.ArgumentGuildChannelMissingGuildError,
18+
message: 'This command can only be used in a server.',
19+
context
20+
});
21+
}
22+
23+
const resolved = resolveGuildNewsThreadChannel(parameter, guild);
24+
if (resolved.success) return this.ok(resolved.value);
25+
return this.error({
26+
parameter,
27+
identifier: resolved.error,
28+
message: 'The given argument did not resolve to a valid announcements thread.',
29+
context: { ...context, guild }
30+
});
2231
}
2332
}

0 commit comments

Comments
 (0)