Skip to content

Commit

Permalink
fix: move stuff to base class and use name option
Browse files Browse the repository at this point in the history
  • Loading branch information
samfundev committed Mar 2, 2024
1 parent 2292237 commit 68d8c87
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 216 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export * from './lib/errors/ArgumentError';
export * from './lib/errors/Identifiers';
export * from './lib/errors/PreconditionError';
export * from './lib/errors/UserError';
export * from './lib/parsers/Args';
export * from './lib/parsers/ChatInputCommandArgs';
export * from './lib/parsers/MessageArgs';
export * from './lib/plugins/Plugin';
export * from './lib/plugins/PluginManager';
Expand Down
88 changes: 72 additions & 16 deletions src/lib/parsers/Args.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities';
import { Result, type Option } from '@sapphire/result';
import type { AnyInteraction, ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities';
import { Result } from '@sapphire/result';
import type {
CategoryChannel,
ChannelType,
Expand All @@ -15,10 +15,13 @@ import type {
VoiceChannel
} from 'discord.js';
import { ArgumentError } from '../errors/ArgumentError';
import type { UserError } from '../errors/UserError';
import { UserError } from '../errors/UserError';
import type { EmojiObject } from '../resolvers/emoji';
import type { Argument, IArgument } from '../structures/Argument';
import type { Awaitable } from '@sapphire/utilities';
import { Identifiers } from '../errors/Identifiers';
import { container } from '@sapphire/pieces';
import type { Command } from '../structures/Command';

export abstract class Args {
public abstract start(): this;
Expand All @@ -28,9 +31,32 @@ export abstract class Args {
public abstract rest<T extends ArgsOptions>(options: T): Promise<InferArgReturnType<T>>;
public abstract repeatResult<T extends ArgsOptions>(options: T): Promise<ArrayResultType<InferArgReturnType<T>>>;
public abstract repeat<T extends ArgsOptions>(options: T): Promise<InferArgReturnType<T>[]>;
public abstract peekResult<T extends ArgsOptions>(options: T): Promise<ResultType<InferArgReturnType<T>>>;
public abstract peek<T extends ArgsOptions>(options: T): Promise<InferArgReturnType<T>>;
// nextMaybe, next, getFlags, getOptionResult, getOption, getOptionsResult, getOptions should only go on message args
public abstract peekResult<T extends PeekArgsOptions>(options: T): Promise<ResultType<InferArgReturnType<T>>>;
public abstract peek<T extends PeekArgsOptions>(options: T): Promise<InferArgReturnType<T>>;

protected unavailableArgument<T>(type: string | IArgument<T>): Result.Err<UserError> {
const name = typeof type === 'string' ? type : type.name;
return Result.err(
new UserError({
identifier: Identifiers.ArgsUnavailable,
message: `The argument "${name}" was not found.`,
context: { name, ...this.toJSON() }
})
);
}

protected missingArguments(): Result.Err<UserError> {
return Result.err(new UserError({ identifier: Identifiers.ArgsMissing, message: 'There are no more arguments.', context: this.toJSON() }));
}

/**
* Resolves an argument.
* @param arg The argument name or {@link IArgument} instance.
*/
protected resolveArgument<T>(arg: keyof ArgType | IArgument<T>): IArgument<T> | undefined {
if (typeof arg === 'object') return arg;
return container.stores.get('arguments').get(arg as string) as IArgument<T> | undefined;
}

/**
* Converts a callback into a usable argument.
Expand All @@ -56,6 +82,46 @@ export abstract class Args {
public static error<T>(options: ArgumentError.Options<T>): Result.Err<ArgumentError<T>> {
return Result.err(new ArgumentError<T>(options));
}

/**
* Defines the `JSON.stringify` override.
*/
public abstract toJSON(): ArgsJson;
}

export interface ArgsJson {
message: Message | AnyInteraction;
command: Command;
commandContext: Record<PropertyKey, unknown>;
}

export interface ArgType {
boolean: boolean;
channel: ChannelTypes;
date: Date;
dmChannel: DMChannel;
emoji: EmojiObject;
float: number;
guildCategoryChannel: CategoryChannel;
guildChannel: GuildBasedChannelTypes;
guildNewsChannel: NewsChannel;
guildNewsThreadChannel: ThreadChannel & { type: ChannelType.AnnouncementThread; parent: NewsChannel | null };
guildPrivateThreadChannel: ThreadChannel & { type: ChannelType.PrivateThread; parent: TextChannel | null };
guildPublicThreadChannel: ThreadChannel & { type: ChannelType.PublicThread; parent: TextChannel | null };
guildStageVoiceChannel: StageChannel;
guildTextChannel: TextChannel;
guildThreadChannel: ThreadChannel;
guildVoiceChannel: VoiceChannel;
hyperlink: URL;
integer: number;
member: GuildMember;
message: Message;
number: number;
role: Role;
string: string;
url: URL;
user: User;
enum: string;
}

export interface ArgsOptions<T = unknown, K extends keyof ArgType = keyof ArgType>
Expand Down Expand Up @@ -125,13 +191,3 @@ export interface ArgType {

export type ResultType<T> = Result<T, UserError | ArgumentError<T>>;
export type ArrayResultType<T> = Result<T[], UserError | ArgumentError<T>>;

/**
* The callback used for {@link Args.nextMaybe} and {@link Args.next}.
*/
export interface ArgsNextCallback<T> {
/**
* The value to be mapped.
*/
(value: string): Option<T>;
}
126 changes: 24 additions & 102 deletions src/lib/parsers/ChatInputCommandArgs.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,21 @@
import type { AnyInteraction, ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities';
import { join, type Parameter } from '@sapphire/lexure';
import { container } from '@sapphire/pieces';
import { Option, Result } from '@sapphire/result';
import type {
CategoryChannel,
ChannelType,
ChatInputCommandInteraction,
CommandInteraction,
DMChannel,
GuildMember,
Message,
NewsChannel,
Role,
StageChannel,
TextChannel,
ThreadChannel,
User,
VoiceChannel
} from 'discord.js';
import type { URL } from 'node:url';
import { Result } from '@sapphire/result';
import type { ChatInputCommandInteraction, CommandInteractionOption } from 'discord.js';
import { ArgumentError } from '../errors/ArgumentError';
import { Identifiers } from '../errors/Identifiers';
import { UserError } from '../errors/UserError';
import type { EmojiObject } from '../resolvers/emoji';
import type { IArgument } from '../structures/Argument';
import { Command } from '../structures/Command';
import { Args, type ArgsOptions, type InferArgReturnType, type PeekArgsOptions, type RepeatArgsOptions } from './Args';
import {
Args,
type ArgsJson,
type ArgsOptions,
type ArrayResultType,
type InferArgReturnType,
type PeekArgsOptions,
type RepeatArgsOptions,
type ResultType
} from './Args';
import type { ChatInputParser } from './ChatInputParser';
import type { ChatInputCommand } from '../types/CommandTypes';

/**
* The argument parser to be used in {@link Command}.
Expand All @@ -40,7 +29,7 @@ export class ChatInputCommandArgs extends Args {
/**
* The command that is being run.
*/
public readonly command: Command;
public readonly command: ChatInputCommand;

/**
* The context of the command being run.
Expand All @@ -57,9 +46,14 @@ export class ChatInputCommandArgs extends Args {
* @see Args#save
* @see Args#restore
*/
private readonly states: number[] = [];

public constructor(interaction: ChatInputCommandInteraction, command: Command, parser: ChatInputParser, context: Record<PropertyKey, unknown>) {
private readonly states: Set<CommandInteractionOption>[] = [];

public constructor(
interaction: ChatInputCommandInteraction,
command: ChatInputCommand,
parser: ChatInputParser,
context: Record<PropertyKey, unknown>
) {
super();
this.interaction = interaction;
this.command = command;
Expand Down Expand Up @@ -126,7 +120,7 @@ export class ChatInputCommandArgs extends Args {
const argument = this.resolveArgument(options.type);
if (!argument) return this.unavailableArgument(options.type);

const result = await this.parser.singleParseAsync(async (arg) =>
const result = await this.parser.singleParseAsync(options.name, async (arg) =>
argument.run(arg, {
args: this,
argument,
Expand Down Expand Up @@ -317,7 +311,7 @@ export class ChatInputCommandArgs extends Args {
const output: InferArgReturnType<T>[] = [];

for (let i = 0, times = options.times ?? Infinity; i < times; i++) {
const result = await this.parser.singleParseAsync(async (arg) =>
const result = await this.parser.singleParseAsync(options.name, async (arg) =>
argument.run(arg, {
args: this,
argument,
Expand Down Expand Up @@ -551,76 +545,4 @@ export class ChatInputCommandArgs extends Args {
public toJSON(): ArgsJson {
return { message: this.interaction, command: this.command, commandContext: this.commandContext };
}

protected unavailableArgument<T>(type: string | IArgument<T>): Result.Err<UserError> {
const name = typeof type === 'string' ? type : type.name;
return Result.err(
new UserError({
identifier: Identifiers.ArgsUnavailable,
message: `The argument "${name}" was not found.`,
context: { name, ...this.toJSON() }
})
);
}

protected missingArguments(): Result.Err<UserError> {
return Result.err(new UserError({ identifier: Identifiers.ArgsMissing, message: 'There are no more arguments.', context: this.toJSON() }));
}

/**
* Resolves an argument.
* @param arg The argument name or {@link IArgument} instance.
*/
private resolveArgument<T>(arg: keyof ArgType | IArgument<T>): IArgument<T> | undefined {
if (typeof arg === 'object') return arg;
return container.stores.get('arguments').get(arg as string) as IArgument<T> | undefined;
}
}

export interface ArgsJson {
message: Message | AnyInteraction;
command: Command;
commandContext: Record<PropertyKey, unknown>;
}

export interface ArgType {
boolean: boolean;
channel: ChannelTypes;
date: Date;
dmChannel: DMChannel;
emoji: EmojiObject;
float: number;
guildCategoryChannel: CategoryChannel;
guildChannel: GuildBasedChannelTypes;
guildNewsChannel: NewsChannel;
guildNewsThreadChannel: ThreadChannel & { type: ChannelType.AnnouncementThread; parent: NewsChannel | null };
guildPrivateThreadChannel: ThreadChannel & { type: ChannelType.PrivateThread; parent: TextChannel | null };
guildPublicThreadChannel: ThreadChannel & { type: ChannelType.PublicThread; parent: TextChannel | null };
guildStageVoiceChannel: StageChannel;
guildTextChannel: TextChannel;
guildThreadChannel: ThreadChannel;
guildVoiceChannel: VoiceChannel;
hyperlink: URL;
integer: number;
member: GuildMember;
message: Message;
number: number;
role: Role;
string: string;
url: URL;
user: User;
enum: string;
}

/**
* The callback used for {@link Args.nextMaybe} and {@link Args.next}.
*/
export interface ArgsNextCallback<T> {
/**
* The value to be mapped.
*/
(value: CommandInteraction): Option<T>;
}

export type ResultType<T> = Result<T, UserError | ArgumentError<T>>;
export type ArrayResultType<T> = Result<T[], UserError | ArgumentError<T>>;
22 changes: 13 additions & 9 deletions src/lib/parsers/ChatInputParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,39 @@ import { Option, Result } from '@sapphire/result';
import { type Parameter } from '@sapphire/lexure';

export class ChatInputParser {
public position: number = 0;
public used: Set<CommandInteractionOption> = new Set();

public constructor(public interaction: CommandInteraction) {}

public get finished(): boolean {
return this.position === this.interaction.options.data.length;
return this.used.size === this.interaction.options.data.length;
}

public reset(): void {
this.position = 0;
this.used.clear();
}

public save(): number {
return this.position;
public save(): Set<CommandInteractionOption> {
return new Set(this.used);
}

public restore(state: number): void {
this.position = state;
public restore(state: Set<CommandInteractionOption>): void {
this.used = state;
}

public async singleParseAsync<T, E>(
name: string,
predicate: (arg: CommandInteractionOption) => Promise<Result<T, E>>,
useAnyways?: boolean
): Promise<Result<T, E | null>> {
if (this.finished) return Result.err(null);

const result = await predicate(this.interaction.options.data[this.position]);
const option = this.interaction.options.data.find((option) => option.name === name);
if (!option) return Result.err(null);

const result = await predicate(option);
if (result.isOk() || useAnyways) {
this.position++;
this.used.add(option);
}
return result;
}
Expand Down
Loading

0 comments on commit 68d8c87

Please sign in to comment.